From 1cb1ac89d77c714024f5525964a1f0700c824a62 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 13 Aug 2020 09:56:35 -0700 Subject: [PATCH 01/52] Scaffold a new topology page --- ui/app/router.js | 2 ++ ui/app/templates/components/gutter-menu.hbs | 1 + ui/app/templates/topology.hbs | 9 +++++++++ 3 files changed, 12 insertions(+) create mode 100644 ui/app/templates/topology.hbs diff --git a/ui/app/router.js b/ui/app/router.js index 12c3c3047..8a6cf648c 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -37,6 +37,8 @@ Router.map(function() { }); }); + this.route('topology'); + this.route('csi', function() { this.route('volumes', function() { this.route('volume', { path: '/:volume_name' }); diff --git a/ui/app/templates/components/gutter-menu.hbs b/ui/app/templates/components/gutter-menu.hbs index eb788f0b9..9d0608d35 100644 --- a/ui/app/templates/components/gutter-menu.hbs +++ b/ui/app/templates/components/gutter-menu.hbs @@ -81,6 +81,7 @@ diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs new file mode 100644 index 000000000..19f060db0 --- /dev/null +++ b/ui/app/templates/topology.hbs @@ -0,0 +1,9 @@ +{{title "Cluster Topology"}} + +
+
+

Cluster topology visualization goes here

+

:D

+
+
+
From f0a096119f2ed65cc052ac9daa6c5d084bd7d09d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 24 Aug 2020 09:59:55 -0700 Subject: [PATCH 02/52] Small cluster example scenario for the topo viz --- ui/app/routes/topology.js | 27 +++++++++++++++++ ui/app/templates/topology.hbs | 14 +++++++++ ui/config/environment.js | 8 ++--- ui/mirage/scenarios/default.js | 2 ++ ui/mirage/scenarios/topo.js | 53 ++++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 ui/app/routes/topology.js create mode 100644 ui/mirage/scenarios/topo.js diff --git a/ui/app/routes/topology.js b/ui/app/routes/topology.js new file mode 100644 index 000000000..97b4f0217 --- /dev/null +++ b/ui/app/routes/topology.js @@ -0,0 +1,27 @@ +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; +import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; +import classic from 'ember-classic-decorator'; +import RSVP from 'rsvp'; + +@classic +export default class TopologyRoute extends Route.extend(WithForbiddenState) { + @service store; + @service system; + + breadcrumbs = [ + { + label: 'Topology', + args: ['topology'], + }, + ]; + + model() { + return RSVP.hash({ + allocations: this.store.findAll('allocation'), + jobs: this.store.findAll('job'), + nodes: this.store.findAll('node'), + }).catch(notifyForbidden(this)); + } +} diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index 19f060db0..585089b36 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -5,5 +5,19 @@

Cluster topology visualization goes here

:D

+ +

Clients

+
    + {{#each model.nodes as |node|}} +
  1. {{node.name}} {{!node.allocations.length}} {{node.resources.cpu}} MHz {{node.resources.memory}} MiB
  2. + {{/each}} +
+ +

Allocations

+
    + {{#each model.allocations as |allocation|}} +
  1. {{allocation.shortId}} {{allocation.node.name}} {{allocation.job.name}}/{{allocation.taskGroup.name}} {{allocation.resources.cpu}} MHz {{allocation.resources.memory}} MiB
  2. + {{/each}} +
diff --git a/ui/config/environment.js b/ui/config/environment.js index 9d758e0a6..65af8d9a6 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -24,11 +24,11 @@ module.exports = function(environment) { }, APP: { - blockingQueries: true, - mirageScenario: 'smallCluster', - mirageWithNamespaces: true, + blockingQueries: false, + mirageScenario: 'topoSmall', + mirageWithNamespaces: false, mirageWithTokens: true, - mirageWithRegions: true, + mirageWithRegions: false, }, }; diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js index fadd4f734..c30dc446a 100644 --- a/ui/mirage/scenarios/default.js +++ b/ui/mirage/scenarios/default.js @@ -1,4 +1,5 @@ import config from 'nomad-ui/config/environment'; +import * as topoScenarios from './topo'; import { pickOne } from '../utils'; const withNamespaces = getConfigValue('mirageWithNamespaces', false); @@ -14,6 +15,7 @@ const allScenarios = { allNodeTypes, everyFeature, emptyCluster, + ...topoScenarios, }; const scenario = getConfigValue('mirageScenario', 'emptyCluster'); diff --git a/ui/mirage/scenarios/topo.js b/ui/mirage/scenarios/topo.js new file mode 100644 index 000000000..d22763ff8 --- /dev/null +++ b/ui/mirage/scenarios/topo.js @@ -0,0 +1,53 @@ +import faker from 'nomad-ui/mirage/faker'; +import { generateNetworks, generatePorts } from '../common'; + +export function topoSmall(server) { + server.createList('agent', 3); + server.createList('node', 12, { + datacenter: 'dc1', + status: 'ready', + resources: { + CPU: 4000, + MemoryMB: 8192, + DiskMB: 10000, + IOPS: 100000, + Networks: generateNetworks(), + Ports: generatePorts(), + }, + }); + + const jobResources = [ + ['M: 256, C: 150'], + ['M: 128, C: 400'], + ['M: 512, C: 100'], + ['M: 256, C: 150'], + ['M: 200, C: 50'], + ['M: 64, C: 100'], + ['M: 128, C: 150'], + ['M: 1024, C: 500'], + ['M: 100, C: 300', 'M: 200, C: 150'], + ['M: 512, C: 250', 'M: 600, C: 200'], + ]; + + jobResources.forEach(spec => { + server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'service', + createAllocations: false, + resourceSpec: spec, + }); + }); + + server.createList('allocation', 35, { + forceRunningClientStatus: true, + }); +} + +export function topoSmallProblems(server) {} + +export function topoMedium(server) {} + +export function topoMediumBatch(server) {} + +export function topoMediumVariadic(server) {} From 2a067d646d04ab9013014aa8b2dce3c3a37c9b5d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 Sep 2020 19:37:13 -0700 Subject: [PATCH 03/52] Scaffold topo viz components --- ui/app/components/topo-viz.js | 15 ++++++++++++ ui/app/components/topo-viz/node.js | 7 ++++++ ui/app/templates/components/topo-viz.hbs | 5 ++++ .../components/topo-viz/datacenter.hbs | 8 +++++++ ui/app/templates/components/topo-viz/node.hbs | 11 +++++++++ ui/app/templates/topology.hbs | 23 +++++-------------- 6 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 ui/app/components/topo-viz.js create mode 100644 ui/app/components/topo-viz/node.js create mode 100644 ui/app/templates/components/topo-viz.hbs create mode 100644 ui/app/templates/components/topo-viz/datacenter.hbs create mode 100644 ui/app/templates/components/topo-viz/node.hbs diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js new file mode 100644 index 000000000..b0d7e11c4 --- /dev/null +++ b/ui/app/components/topo-viz.js @@ -0,0 +1,15 @@ +import Component from '@glimmer/component'; + +export default class TopoViz extends Component { + get datacenters() { + const datacentersMap = this.args.nodes.reduce((datacenters, node) => { + if (!datacenters[node.datacenter]) datacenters[node.datacenter] = []; + datacenters[node.datacenter].push(node); + return datacenters; + }, {}); + + return Object.keys(datacentersMap) + .map(key => ({ name: key, nodes: datacentersMap[key] })) + .sortBy('name'); + } +} diff --git a/ui/app/components/topo-viz/node.js b/ui/app/components/topo-viz/node.js new file mode 100644 index 000000000..3e28f1304 --- /dev/null +++ b/ui/app/components/topo-viz/node.js @@ -0,0 +1,7 @@ +import Component from '@glimmer/component'; + +export default class TopoViz extends Component { + get count() { + return this.args.node.get('allocations.length'); + } +} diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs new file mode 100644 index 000000000..40860cb0b --- /dev/null +++ b/ui/app/templates/components/topo-viz.hbs @@ -0,0 +1,5 @@ +
+ {{#each this.datacenters as |dc|}} + + {{/each}} +
diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs new file mode 100644 index 000000000..60aa94605 --- /dev/null +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -0,0 +1,8 @@ +
+
{{@datacenter}}
+
+ {{#each @nodes as |node|}} + + {{/each}} +
+
diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs new file mode 100644 index 000000000..0e0581013 --- /dev/null +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -0,0 +1,11 @@ +
+ Node! {{@node.name}} ({{this.count}} allocs) ({{@node.resources.memory}} MiB, {{@node.resources.cpu}} Mhz) +
    + {{#each @node.allocations as |allocation|}} +
  • + {{allocation.name}} {{allocation.shortId}} + ({{allocation.resources.memory}} MiB, {{allocation.resources.cpu}} Mhz) +
  • + {{/each}} +
+
diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index 585089b36..a501d89d3 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -1,23 +1,12 @@ {{title "Cluster Topology"}}
-
-

Cluster topology visualization goes here

-

:D

+
+
Cluster Metrics
+
+ Aggregate metrics go here. +
- -

Clients

-
    - {{#each model.nodes as |node|}} -
  1. {{node.name}} {{!node.allocations.length}} {{node.resources.cpu}} MHz {{node.resources.memory}} MiB
  2. - {{/each}} -
- -

Allocations

-
    - {{#each model.allocations as |allocation|}} -
  1. {{allocation.shortId}} {{allocation.node.name}} {{allocation.job.name}}/{{allocation.taskGroup.name}} {{allocation.resources.cpu}} MHz {{allocation.resources.memory}} MiB
  2. - {{/each}} -
+
From 640f1b6716e6f342847470112fd482c062a044bb Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 Sep 2020 00:42:20 -0700 Subject: [PATCH 04/52] Add ember modifiers addon --- ui/package.json | 1 + ui/yarn.lock | 910 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 908 insertions(+), 3 deletions(-) diff --git a/ui/package.json b/ui/package.json index 35a3ca20a..8ea295dea 100644 --- a/ui/package.json +++ b/ui/package.json @@ -84,6 +84,7 @@ "ember-inline-svg": "^0.3.0", "ember-load-initializers": "^2.1.1", "ember-maybe-import-regenerator": "^0.1.6", + "ember-modifier": "^2.1.0", "ember-moment": "^7.8.1", "ember-overridable-computed": "^1.0.0", "ember-page-title": "^5.0.2", diff --git a/ui/yarn.lock b/ui/yarn.lock index 75b7a7316..70a7edf24 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -30,6 +30,13 @@ dependencies: "@babel/highlight" "^7.10.3" +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/compat-data@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.1.tgz#b1085ffe72cd17bf2c0ee790fc09f9626011b2db" @@ -48,6 +55,15 @@ invariant "^2.2.4" semver "^5.5.0" +"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" + integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== + dependencies: + browserslist "^4.12.0" + invariant "^2.2.4" + semver "^5.5.0" + "@babel/core@^7.0.0", "@babel/core@^7.1.6", "@babel/core@^7.3.3", "@babel/core@^7.3.4": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.0.tgz#248fd6874b7d755010bfe61f557461d4f446d9e9" @@ -90,6 +106,28 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.11.0": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651" + integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.6" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.5" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.5" + "@babel/types" "^7.11.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/core@^7.2.2": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" @@ -152,6 +190,15 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/generator@^7.11.5", "@babel/generator@^7.11.6": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" + integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== + dependencies: + "@babel/types" "^7.11.5" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/generator@^7.4.0", "@babel/generator@^7.6.2": version "7.6.2" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.2.tgz#dac8a3c2df118334c2a29ff3446da1636a8f8c03" @@ -187,6 +234,13 @@ dependencies: "@babel/types" "^7.10.1" +"@babel/helper-annotate-as-pure@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" + integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f" @@ -203,6 +257,14 @@ "@babel/helper-explode-assignable-expression" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" + integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-call-delegate@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43" @@ -223,6 +285,17 @@ levenary "^1.1.1" semver "^5.5.0" +"@babel/helper-compilation-targets@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" + integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== + dependencies: + "@babel/compat-data" "^7.10.4" + browserslist "^4.12.0" + invariant "^2.2.4" + levenary "^1.1.1" + semver "^5.5.0" + "@babel/helper-create-class-features-plugin@^7.10.1": version "7.10.2" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.2.tgz#7474295770f217dbcf288bf7572eb213db46ee67" @@ -247,6 +320,18 @@ "@babel/helper-replace-supers" "^7.10.1" "@babel/helper-split-export-declaration" "^7.10.1" +"@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-create-class-features-plugin@^7.4.0", "@babel/helper-create-class-features-plugin@^7.5.5": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.6.0.tgz#769711acca889be371e9bc2eb68641d55218021f" @@ -280,6 +365,15 @@ "@babel/helper-regex" "^7.10.1" regexpu-core "^4.7.0" +"@babel/helper-create-regexp-features-plugin@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" + integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-regex" "^7.10.4" + regexpu-core "^4.7.0" + "@babel/helper-define-map@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.1.tgz#5e69ee8308648470dd7900d159c044c10285221d" @@ -298,6 +392,15 @@ "@babel/types" "^7.10.3" lodash "^4.17.13" +"@babel/helper-define-map@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" + integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/types" "^7.10.5" + lodash "^4.17.19" + "@babel/helper-define-map@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a" @@ -332,6 +435,13 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-explode-assignable-expression@^7.10.4": + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b" + integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-function-name@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" @@ -359,6 +469,15 @@ "@babel/template" "^7.10.3" "@babel/types" "^7.10.3" +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-get-function-arity@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" @@ -380,6 +499,13 @@ dependencies: "@babel/types" "^7.10.3" +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-hoist-variables@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.1.tgz#7e77c82e5dcae1ebf123174c385aaadbf787d077" @@ -394,6 +520,13 @@ dependencies: "@babel/types" "^7.10.3" +"@babel/helper-hoist-variables@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" + integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-hoist-variables@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a" @@ -422,6 +555,13 @@ dependencies: "@babel/types" "^7.10.3" +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" + integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== + dependencies: + "@babel/types" "^7.11.0" + "@babel/helper-member-expression-to-functions@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz#1fb5b8ec4453a93c439ee9fe3aeea4a84b76b590" @@ -450,6 +590,13 @@ dependencies: "@babel/types" "^7.10.3" +"@babel/helper-module-imports@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" + integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-module-transforms@^7.1.0": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8" @@ -475,6 +622,19 @@ "@babel/types" "^7.10.1" lodash "^4.17.13" +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" + integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/template" "^7.10.4" + "@babel/types" "^7.11.0" + lodash "^4.17.19" + "@babel/helper-module-transforms@^7.4.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a" @@ -508,6 +668,13 @@ dependencies: "@babel/types" "^7.10.3" +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-plugin-utils@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" @@ -523,6 +690,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.3.tgz#aac45cccf8bc1873b99a85f34bceef3beb5d3244" integrity sha512-j/+j8NAWUTxOtx4LKHybpSClxHoq6I91DQ/mKgAXn5oNUPIUiGppjPIX3TDtJWPrdfP9Kfl7e4fgVMiQR9VE/g== +"@babel/helper-plugin-utils@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + "@babel/helper-regex@^7.0.0": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2" @@ -537,6 +709,13 @@ dependencies: lodash "^4.17.13" +"@babel/helper-regex@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" + integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== + dependencies: + lodash "^4.17.19" + "@babel/helper-regex@^7.4.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351" @@ -577,6 +756,16 @@ "@babel/traverse" "^7.10.3" "@babel/types" "^7.10.3" +"@babel/helper-remap-async-to-generator@^7.10.4": + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d" + integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-wrap-function" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27" @@ -597,6 +786,16 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-replace-supers@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz#f84ce43df031222d2bad068d2626cb5799c34bc2" @@ -623,6 +822,21 @@ "@babel/template" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-simple-access@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" + integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== + dependencies: + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-skip-transparent-expression-wrappers@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" + integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q== + dependencies: + "@babel/types" "^7.11.0" + "@babel/helper-split-export-declaration@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" @@ -630,6 +844,13 @@ dependencies: "@babel/types" "^7.10.1" +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== + dependencies: + "@babel/types" "^7.11.0" + "@babel/helper-split-export-declaration@^7.4.0", "@babel/helper-split-export-declaration@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" @@ -647,6 +868,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz#60d9847f98c4cea1b279e005fdb7c28be5412d15" integrity sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw== +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + "@babel/helper-wrap-function@^7.1.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" @@ -667,6 +893,16 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-wrap-function@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" + integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helpers@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.1.tgz#a6827b7cb975c9d9cef5fd61d919f60d8844a973" @@ -676,6 +912,15 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helpers@^7.4.0": version "7.6.2" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.2.tgz#681ffe489ea4dcc55f23ce469e58e59c1c045153" @@ -721,6 +966,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.10.1", "@babel/parser@^7.10.2", "@babel/parser@^7.7.0": version "7.10.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.2.tgz#871807f10442b92ff97e4783b9b54f6a0ca812d0" @@ -731,6 +985,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.3.tgz#7e71d892b0d6e7d04a1af4c3c79d72c1f10f5315" integrity sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA== +"@babel/parser@^7.10.4", "@babel/parser@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" + integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== + "@babel/parser@^7.3.4", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" @@ -759,6 +1018,15 @@ "@babel/helper-remap-async-to-generator" "^7.10.3" "@babel/plugin-syntax-async-generators" "^7.8.0" +"@babel/plugin-proposal-async-generator-functions@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" + integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -784,6 +1052,14 @@ "@babel/helper-create-class-features-plugin" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-proposal-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" + integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-class-properties@^7.3.3": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4" @@ -809,6 +1085,15 @@ "@babel/helper-plugin-utils" "^7.10.3" "@babel/plugin-syntax-decorators" "^7.10.1" +"@babel/plugin-proposal-decorators@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4" + integrity sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-decorators" "^7.10.4" + "@babel/plugin-proposal-decorators@^7.3.0": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0" @@ -835,6 +1120,14 @@ "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-dynamic-import" "^7.8.0" +"@babel/plugin-proposal-dynamic-import@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" + integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-proposal-dynamic-import@^7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506" @@ -843,6 +1136,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-dynamic-import" "^7.2.0" +"@babel/plugin-proposal-export-namespace-from@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" + integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-proposal-json-strings@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz#b1e691ee24c651b5a5e32213222b2379734aff09" @@ -851,6 +1152,14 @@ "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-json-strings" "^7.8.0" +"@babel/plugin-proposal-json-strings@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" + integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-proposal-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" @@ -859,6 +1168,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" +"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" + integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1", "@babel/plugin-proposal-nullish-coalescing-operator@^7.4.4": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz#02dca21673842ff2fe763ac253777f235e9bbf78" @@ -867,6 +1184,14 @@ "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" + integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-proposal-numeric-separator@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz#a9a38bc34f78bdfd981e791c27c6fdcec478c123" @@ -875,6 +1200,14 @@ "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-numeric-separator" "^7.10.1" +"@babel/plugin-proposal-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" + integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.1.tgz#cba44908ac9f142650b4a65b8aa06bf3478d5fb6" @@ -893,6 +1226,15 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.10.1" +"@babel/plugin-proposal-object-rest-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" + integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread@^7.3.2", "@babel/plugin-proposal-object-rest-spread@^7.6.2": version "7.6.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.6.2.tgz#8ffccc8f3a6545e9f78988b6bf4fe881b88e8096" @@ -925,6 +1267,14 @@ "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" +"@babel/plugin-proposal-optional-catch-binding@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" + integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" @@ -949,6 +1299,15 @@ "@babel/helper-plugin-utils" "^7.10.3" "@babel/plugin-syntax-optional-chaining" "^7.8.0" +"@babel/plugin-proposal-optional-chaining@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" + integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-proposal-private-methods@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz#ed85e8058ab0fe309c3f448e5e1b73ca89cdb598" @@ -957,6 +1316,14 @@ "@babel/helper-create-class-features-plugin" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-proposal-private-methods@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" + integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz#dc04feb25e2dd70c12b05d680190e138fa2c0c6f" @@ -965,6 +1332,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-proposal-unicode-property-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" + integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78" @@ -1004,6 +1379,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-syntax-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" + integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-decorators@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.1.tgz#16b869c4beafc9a442565147bda7ce0967bd4f13" @@ -1011,6 +1393,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-syntax-decorators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz#6853085b2c429f9d322d02f5a635018cdeb2360c" + integrity sha512-2NaoC6fAk2VMdhY1eerkfHV+lVYC1u8b+jmRJISqANCJlTxYy19HGdIkkQtix2UtkcPuPu+IlDgrVseZnU03bw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-decorators@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.2.0.tgz#c50b1b957dcc69e4b1127b65e1c33eef61570c1b" @@ -1032,6 +1421,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" @@ -1046,6 +1442,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" @@ -1060,6 +1463,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" @@ -1102,6 +1512,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-syntax-top-level-await@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" + integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript@^7.10.1", "@babel/plugin-syntax-typescript@^7.8.3": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.1.tgz#5e82bc27bb4202b93b949b029e699db536733810" @@ -1109,6 +1526,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-syntax-typescript@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz#2f55e770d3501e83af217d782cb7517d7bb34d25" + integrity sha512-oSAEz1YkBCAKr5Yiq8/BNtvSAPwkp/IyUnwZogd8p+F0RuYQQrLeRUzIQhueQTTBy/F+a40uS7OFKxnkRvmvFQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript@^7.2.0": version "7.3.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz#a7cc3f66119a9f7ebe2de5383cce193473d65991" @@ -1123,6 +1547,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-arrow-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" + integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-arrow-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" @@ -1139,6 +1570,15 @@ "@babel/helper-plugin-utils" "^7.10.1" "@babel/helper-remap-async-to-generator" "^7.10.1" +"@babel/plugin-transform-async-to-generator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" + integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" + "@babel/plugin-transform-async-to-generator@^7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e" @@ -1155,6 +1595,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-block-scoped-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" + integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-block-scoped-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" @@ -1170,6 +1617,13 @@ "@babel/helper-plugin-utils" "^7.10.1" lodash "^4.17.13" +"@babel/plugin-transform-block-scoping@^7.10.4": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" + integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-block-scoping@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz#c13279fabf6b916661531841a23c4b7dae29646d" @@ -1214,6 +1668,20 @@ "@babel/helper-split-export-declaration" "^7.10.1" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" + integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-define-map" "^7.10.4" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + globals "^11.1.0" + "@babel/plugin-transform-classes@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz#0ce4094cdafd709721076d3b9c38ad31ca715eb6" @@ -1256,6 +1724,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.3" +"@babel/plugin-transform-computed-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" + integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-computed-properties@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da" @@ -1270,6 +1745,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-destructuring@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" + integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-destructuring@^7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a" @@ -1292,6 +1774,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-dotall-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" + integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" @@ -1317,6 +1807,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-duplicate-keys@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" + integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-duplicate-keys@^7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853" @@ -1332,6 +1829,14 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-exponentiation-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" + integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" @@ -1347,6 +1852,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-for-of@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" + integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-for-of@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556" @@ -1362,6 +1874,14 @@ "@babel/helper-function-name" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" + integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-function-name@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad" @@ -1377,6 +1897,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" + integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-literals@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1" @@ -1391,6 +1918,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-member-expression-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" + integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-member-expression-literals@^7.2.0", "@babel/plugin-transform-modules-amd@^7.0.0": name "@babel/plugin-transform-member-expression-literals" version "7.2.0" @@ -1408,6 +1942,15 @@ "@babel/helper-plugin-utils" "^7.10.1" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-amd@^7.10.4", "@babel/plugin-transform-modules-amd@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" + integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== + dependencies: + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-amd@^7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91" @@ -1427,6 +1970,16 @@ "@babel/helper-simple-access" "^7.10.1" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-commonjs@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" + integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== + dependencies: + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-commonjs@^7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74" @@ -1467,6 +2020,16 @@ "@babel/helper-plugin-utils" "^7.10.3" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-systemjs@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" + integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== + dependencies: + "@babel/helper-hoist-variables" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-systemjs@^7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249" @@ -1484,6 +2047,14 @@ "@babel/helper-module-transforms" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-modules-umd@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" + integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== + dependencies: + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-modules-umd@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" @@ -1499,6 +2070,13 @@ dependencies: "@babel/helper-create-regexp-features-plugin" "^7.8.3" +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" + integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz#9d269fd28a370258199b4294736813a60bbdd106" @@ -1527,6 +2105,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-new-target@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" + integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-new-target@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5" @@ -1549,6 +2134,14 @@ "@babel/helper-plugin-utils" "^7.10.1" "@babel/helper-replace-supers" "^7.10.1" +"@babel/plugin-transform-object-super@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" + integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/plugin-transform-object-super@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz#b35d4c10f56bab5d650047dad0f1d8e8814b6598" @@ -1573,6 +2166,14 @@ "@babel/helper-get-function-arity" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-parameters@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" + integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-parameters@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16" @@ -1589,6 +2190,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-property-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" + integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-property-literals@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz#03e33f653f5b25c4eb572c98b9485055b389e905" @@ -1618,6 +2226,13 @@ dependencies: regenerator-transform "^0.14.2" +"@babel/plugin-transform-regenerator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" + integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== + dependencies: + regenerator-transform "^0.14.2" + "@babel/plugin-transform-regenerator@^7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz#629dc82512c55cee01341fb27bdfcb210354680f" @@ -1632,6 +2247,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-reserved-words@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" + integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-reserved-words@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz#4792af87c998a49367597d07fedf02636d2e1634" @@ -1649,6 +2271,16 @@ resolve "^1.8.1" semver "^5.5.1" +"@babel/plugin-transform-runtime@^7.11.0": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.5.tgz#f108bc8e0cf33c37da031c097d1df470b3a293fc" + integrity sha512-9aIoee+EhjySZ6vY5hnLjigHzunBlscx9ANKutkeWTJTx6m5Rbq6Ic01tLvO54lSusR+BxV7u4UDdCmXv5aagg== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + resolve "^1.8.1" + semver "^5.5.1" + "@babel/plugin-transform-runtime@^7.2.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.0.tgz#45242c2c9281158c5f06d25beebac63e498a284e" @@ -1676,6 +2308,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-shorthand-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" + integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-shorthand-properties@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0" @@ -1690,6 +2329,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" + integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + "@babel/plugin-transform-spread@^7.2.0": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406" @@ -1712,6 +2359,14 @@ "@babel/helper-plugin-utils" "^7.10.1" "@babel/helper-regex" "^7.10.1" +"@babel/plugin-transform-sticky-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" + integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-regex" "^7.10.4" + "@babel/plugin-transform-sticky-regex@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1" @@ -1736,6 +2391,14 @@ "@babel/helper-annotate-as-pure" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.3" +"@babel/plugin-transform-template-literals@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" + integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-template-literals@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0" @@ -1751,6 +2414,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-typeof-symbol@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" + integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-typeof-symbol@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2" @@ -1767,6 +2437,15 @@ "@babel/helper-plugin-utils" "^7.10.3" "@babel/plugin-syntax-typescript" "^7.10.1" +"@babel/plugin-transform-typescript@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.11.0.tgz#2b4879676af37342ebb278216dd090ac67f13abb" + integrity sha512-edJsNzTtvb3MaXQwj8403B7mZoGu9ElDJQZOKjGUnvilquxBA3IQoEIOvkX/1O8xfAsnHS/oQhe2w/IXrr+w0w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript" "^7.10.4" + "@babel/plugin-transform-typescript@^7.9.0": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.1.tgz#2c54daea231f602468686d9faa76f182a94507a6" @@ -1809,6 +2488,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-unicode-escapes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" + integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-unicode-regex@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz#6b58f2aea7b68df37ac5025d9c88752443a6b43f" @@ -1817,6 +2503,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.1" "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-unicode-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" + integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-unicode-regex@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" @@ -1851,6 +2545,14 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" +"@babel/polyfill@^7.10.4": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.11.5.tgz#df550b2ec53abbc2ed599367ec59e64c7a707bb5" + integrity sha512-FunXnE0Sgpd61pKSj2OSOs1D44rKTD3pGOfGilZ6LGrrIH0QEtJlTjqOqdF8Bs98JmjfGhni2BBkTfv9KcKJ9g== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.4" + "@babel/preset-env@^7.0.0": version "7.5.4" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.4.tgz#64bc15041a3cbb0798930319917e70fcca57713d" @@ -1977,6 +2679,80 @@ levenary "^1.1.1" semver "^5.5.0" +"@babel/preset-env@^7.11.0": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272" + integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA== + dependencies: + "@babel/compat-data" "^7.11.0" + "@babel/helper-compilation-targets" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-async-generator-functions" "^7.10.4" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-dynamic-import" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-json-strings" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" + "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.11.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" + "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-private-methods" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.10.4" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-syntax-top-level-await" "^7.10.4" + "@babel/plugin-transform-arrow-functions" "^7.10.4" + "@babel/plugin-transform-async-to-generator" "^7.10.4" + "@babel/plugin-transform-block-scoped-functions" "^7.10.4" + "@babel/plugin-transform-block-scoping" "^7.10.4" + "@babel/plugin-transform-classes" "^7.10.4" + "@babel/plugin-transform-computed-properties" "^7.10.4" + "@babel/plugin-transform-destructuring" "^7.10.4" + "@babel/plugin-transform-dotall-regex" "^7.10.4" + "@babel/plugin-transform-duplicate-keys" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator" "^7.10.4" + "@babel/plugin-transform-for-of" "^7.10.4" + "@babel/plugin-transform-function-name" "^7.10.4" + "@babel/plugin-transform-literals" "^7.10.4" + "@babel/plugin-transform-member-expression-literals" "^7.10.4" + "@babel/plugin-transform-modules-amd" "^7.10.4" + "@babel/plugin-transform-modules-commonjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-umd" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" + "@babel/plugin-transform-new-target" "^7.10.4" + "@babel/plugin-transform-object-super" "^7.10.4" + "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-transform-property-literals" "^7.10.4" + "@babel/plugin-transform-regenerator" "^7.10.4" + "@babel/plugin-transform-reserved-words" "^7.10.4" + "@babel/plugin-transform-shorthand-properties" "^7.10.4" + "@babel/plugin-transform-spread" "^7.11.0" + "@babel/plugin-transform-sticky-regex" "^7.10.4" + "@babel/plugin-transform-template-literals" "^7.10.4" + "@babel/plugin-transform-typeof-symbol" "^7.10.4" + "@babel/plugin-transform-unicode-escapes" "^7.10.4" + "@babel/plugin-transform-unicode-regex" "^7.10.4" + "@babel/preset-modules" "^0.1.3" + "@babel/types" "^7.11.5" + browserslist "^4.12.0" + core-js-compat "^3.6.2" + invariant "^2.2.2" + levenary "^1.1.1" + semver "^5.5.0" + "@babel/preset-env@^7.4.5": version "7.6.2" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.2.tgz#abbb3ed785c7fe4220d4c82a53621d71fc0c75d3" @@ -2143,6 +2919,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.11.0": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.2.0": version "7.5.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b" @@ -2198,6 +2981,15 @@ "@babel/parser" "^7.10.3" "@babel/types" "^7.10.3" +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/template@^7.4.0", "@babel/template@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" @@ -2252,6 +3044,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" + integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.11.5" + "@babel/types" "^7.11.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/traverse@^7.2.4", "@babel/traverse@^7.3.4", "@babel/traverse@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" @@ -2309,6 +3116,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" + integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@babel/types@^7.3.2", "@babel/types@^7.3.4", "@babel/types@^7.4.4", "@babel/types@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" @@ -4601,6 +5417,13 @@ babel-plugin-ember-modules-api-polyfill@^2.8.0, babel-plugin-ember-modules-api-p dependencies: ember-rfc176-data "^0.3.9" +babel-plugin-ember-modules-api-polyfill@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-3.1.1.tgz#c6e9ede43b64c4e36512f260e42e829b071d9b4f" + integrity sha512-hRTnr59fJ6cIiSiSgQLM9QRiVv/RrBAYRYggCPQDj4dvYhOWZeoX6e+1jFY1qC3tJnSDuMWu3OrDciSIi1MJ0A== + dependencies: + ember-rfc176-data "^0.3.15" + babel-plugin-emotion@^10.0.14, babel-plugin-emotion@^10.0.17: version "10.0.21" resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.21.tgz#9ebeb12edeea3e60a5476b0e07c9868605e65968" @@ -5538,6 +6361,24 @@ broccoli-babel-transpiler@^7.5.0: rsvp "^4.8.4" workerpool "^3.1.1" +broccoli-babel-transpiler@^7.7.0: + version "7.7.0" + resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-7.7.0.tgz#271d401e713bfd338d5ef0435d3c4c68f6eddd2a" + integrity sha512-U8Cmnv0/AcQKehiIVi6UDzqq3jqhAEbY9CvOW5vdeNRmYhFpK6bXPmVczS/nUz5g4KsPc/FdnC3zbU6yVf4e7w== + dependencies: + "@babel/core" "^7.11.0" + "@babel/polyfill" "^7.10.4" + broccoli-funnel "^2.0.2" + broccoli-merge-trees "^3.0.2" + broccoli-persistent-filter "^2.2.1" + clone "^2.1.2" + hash-for-dep "^1.4.7" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.9" + json-stable-stringify "^1.0.1" + rsvp "^4.8.4" + workerpool "^3.1.1" + broccoli-builder@^0.18.14: version "0.18.14" resolved "https://registry.yarnpkg.com/broccoli-builder/-/broccoli-builder-0.18.14.tgz#4b79e2f844de11a4e1b816c3f49c6df4776c312d" @@ -8331,6 +9172,38 @@ ember-cli-babel@^7.11.1: rimraf "^3.0.1" semver "^5.5.0" +ember-cli-babel@^7.22.1: + version "7.22.1" + resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.22.1.tgz#cad28b89cf0e184c93b863d09bc5ba4ce1d2e453" + integrity sha512-kCT8WbC1AYFtyOpU23ESm22a+gL6fWv8Nzwe8QFQ5u0piJzM9MEudfbjADEaoyKTrjMQTDsrWwEf3yjggDsOng== + dependencies: + "@babel/core" "^7.11.0" + "@babel/helper-compilation-targets" "^7.10.4" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-decorators" "^7.10.5" + "@babel/plugin-transform-modules-amd" "^7.10.5" + "@babel/plugin-transform-runtime" "^7.11.0" + "@babel/plugin-transform-typescript" "^7.11.0" + "@babel/polyfill" "^7.10.4" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.0" + amd-name-resolver "^1.2.1" + babel-plugin-debug-macros "^0.3.3" + babel-plugin-ember-data-packages-polyfill "^0.1.2" + babel-plugin-ember-modules-api-polyfill "^3.1.1" + babel-plugin-module-resolver "^3.1.1" + broccoli-babel-transpiler "^7.7.0" + broccoli-debug "^0.6.4" + broccoli-funnel "^2.0.1" + broccoli-source "^1.1.0" + clone "^2.1.2" + ember-cli-babel-plugin-helpers "^1.1.0" + ember-cli-version-checker "^4.1.0" + ensure-posix-path "^1.0.2" + fixturify-project "^1.10.0" + rimraf "^3.0.1" + semver "^5.5.0" + ember-cli-clipboard@^0.13.0: version "0.13.0" resolved "https://registry.yarnpkg.com/ember-cli-clipboard/-/ember-cli-clipboard-0.13.0.tgz#47d3de3aec09987409c162cbff36f966a2c138b7" @@ -8655,7 +9528,7 @@ ember-cli-uglify@^3.0.0: broccoli-uglify-sourcemap "^3.1.0" lodash.defaultsdeep "^4.6.0" -ember-cli-version-checker@5.1.1: +ember-cli-version-checker@5.1.1, ember-cli-version-checker@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-5.1.1.tgz#3185c526c14671609cbd22ab0d0925787fc84f3d" integrity sha512-YziSW1MgOuVdJSyUY2CKSC4vXrGQIHF6FgygHkJOxYGjZNQYwf5MK0sbliKatvJf7kzDSnXs+r8JLrD74W/A8A== @@ -8795,7 +9668,7 @@ ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.2.0: ember-cli-version-checker "^2.1.1" semver "^5.4.1" -ember-compatibility-helpers@^1.1.2: +ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.1.tgz#87c92c4303f990ff455c28ca39fb3ee11441aa16" integrity sha512-6wzYvnhg1ihQUT5yGqnLtleq3Nv5KNv79WhrEuNU9SwR4uIxCO+KpyC7r3d5VI0EM7/Nmv9Nd0yTkzmTMdVG1A== @@ -8874,6 +9747,15 @@ ember-decorators@^6.1.1: "@ember-decorators/object" "^6.1.1" ember-cli-babel "^7.7.3" +ember-destroyable-polyfill@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ember-destroyable-polyfill/-/ember-destroyable-polyfill-2.0.1.tgz#391cd95a99debaf24148ce953054008d00f151a6" + integrity sha512-hyK+/GPWOIxM1vxnlVMknNl9E5CAFVbcxi8zPiM0vCRwHiFS8Wuj7PfthZ1/OFitNNv7ITTeU8hxqvOZVsrbnQ== + dependencies: + ember-cli-babel "^7.22.1" + ember-cli-version-checker "^5.1.1" + ember-compatibility-helpers "^1.2.1" + ember-element-helper@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ember-element-helper/-/ember-element-helper-0.2.0.tgz#eacdf4d8507d6708812623206e24ad37bad487e7" @@ -8993,7 +9875,7 @@ ember-maybe-in-element@^0.4.0: dependencies: ember-cli-babel "^7.1.0" -ember-modifier-manager-polyfill@^1.1.0: +ember-modifier-manager-polyfill@^1.1.0, ember-modifier-manager-polyfill@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/ember-modifier-manager-polyfill/-/ember-modifier-manager-polyfill-1.2.0.tgz#cf4444e11a42ac84f5c8badd85e635df57565dda" integrity sha512-bnaKF1LLKMkBNeDoetvIJ4vhwRPKIIumWr6dbVuW6W6p4QV8ZiO+GdF8J7mxDNlog9CeL9Z/7wam4YS86G8BYA== @@ -9002,6 +9884,18 @@ ember-modifier-manager-polyfill@^1.1.0: ember-cli-version-checker "^2.1.2" ember-compatibility-helpers "^1.2.0" +ember-modifier@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-2.1.0.tgz#99d85995caad8789220dc3208fb5ded45647dccf" + integrity sha512-tVmRcEloYg8AZHheEMhBhzX64r7n6AFLXG69L/jiHePvQzet9mjV18YiIPStQf+fdjTAO25S6yzNPDP3zQjWtQ== + dependencies: + ember-cli-babel "^7.22.1" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-string-utils "^1.1.0" + ember-cli-typescript "^3.1.3" + ember-destroyable-polyfill "^2.0.1" + ember-modifier-manager-polyfill "^1.2.0" + ember-moment@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/ember-moment/-/ember-moment-7.8.1.tgz#6f77cf941d1a92e231b2f4b810e113b2fae50c5f" @@ -9101,6 +9995,11 @@ ember-rfc176-data@^0.3.13: resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.13.tgz#ed1712a26e65fec703655f35410414aa1982cf3b" integrity sha512-m9JbwQlT6PjY7x/T8HslnXP7Sz9bx/pz3FrNfNi2NesJnbNISly0Lix6NV1fhfo46572cpq4jrM+/6yYlMefTQ== +ember-rfc176-data@^0.3.15: + version "0.3.15" + resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.15.tgz#af3f1da5a0339b6feda380edc2f7190e0f416c2d" + integrity sha512-GPKa7zRDBblRy0orxTXt5yrpp/Pf5CkuRFSIR8qMFDww0CqCKjCRwdZnWYzCM4kAEfZnXRIDDefe1tBaFw7v7w== + ember-router-generator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-2.0.0.tgz#d04abfed4ba8b42d166477bbce47fccc672dbde0" @@ -12754,6 +13653,11 @@ lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== +lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" From fcf8adb76e888ad565c87d1033848959912d5cdb Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 Sep 2020 00:42:51 -0700 Subject: [PATCH 05/52] Quick window resize modifier --- ui/app/modifiers/window-resize.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 ui/app/modifiers/window-resize.js diff --git a/ui/app/modifiers/window-resize.js b/ui/app/modifiers/window-resize.js new file mode 100644 index 000000000..f70b20976 --- /dev/null +++ b/ui/app/modifiers/window-resize.js @@ -0,0 +1,10 @@ +import { modifier } from 'ember-modifier'; + +export default modifier(function windowResize(element, [handler]) { + const boundHandler = ev => handler(element, ev); + window.addEventListener('resize', boundHandler); + + return () => { + window.removeEventListener('resize', boundHandler); + }; +}); From b347141e0cb99f796f4293fdaa9ba68f3077d1cf Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 Sep 2020 00:43:27 -0700 Subject: [PATCH 06/52] Prototype of the topo viz - Plot all datacenters - For each datacenter, plot all nodes - For each node, plot all allocations by memory and cpu - For empty nodes, highlight the emptiness - When hovering over allocations, give them visual focus --- ui/app/components/topo-viz.js | 19 +++ ui/app/components/topo-viz/datacenter.js | 46 +++++++ ui/app/components/topo-viz/node.js | 119 +++++++++++++++++- ui/app/models/allocation.js | 5 + ui/app/styles/charts.scss | 1 + ui/app/styles/charts/topo-viz-node.scss | 46 +++++++ ui/app/templates/components/topo-viz.hbs | 10 +- .../components/topo-viz/datacenter.hbs | 18 ++- ui/app/templates/components/topo-viz/node.hbs | 70 +++++++++-- 9 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 ui/app/components/topo-viz/datacenter.js create mode 100644 ui/app/styles/charts/topo-viz-node.scss diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index b0d7e11c4..0e6d7b032 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -1,6 +1,14 @@ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { scaleLinear } from 'd3-scale'; +import { max } from 'd3-array'; +import RSVP from 'rsvp'; export default class TopoViz extends Component { + @tracked heightScale = null; + @tracked isLoaded = false; + get datacenters() { const datacentersMap = this.args.nodes.reduce((datacenters, node) => { if (!datacenters[node.datacenter]) datacenters[node.datacenter] = []; @@ -12,4 +20,15 @@ export default class TopoViz extends Component { .map(key => ({ name: key, nodes: datacentersMap[key] })) .sortBy('name'); } + + @action + async loadNodes() { + await RSVP.all(this.args.nodes.map(node => node.reload())); + + // TODO: Make the range dynamic based on the extent of the domain + this.heightScale = scaleLinear() + .range([15, 50]) + .domain([0, max(this.args.nodes.map(node => node.resources.memory))]); + this.isLoaded = true; + } } diff --git a/ui/app/components/topo-viz/datacenter.js b/ui/app/components/topo-viz/datacenter.js new file mode 100644 index 000000000..0d224353c --- /dev/null +++ b/ui/app/components/topo-viz/datacenter.js @@ -0,0 +1,46 @@ +import RSVP from 'rsvp'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +export default class TopoVizNode extends Component { + @tracked scheduledAllocations = []; + @tracked aggregatedNodeResources = { cpu: 0, memory: 0 }; + @tracked isLoaded = false; + + get aggregateNodeResources() { + return this.args.nodes.mapBy('resources'); + } + + get aggregatedAllocationResources() { + return this.scheduledAllocations.mapBy('resources').reduce( + (totals, allocation) => { + totals.cpu += allocation.cpu; + totals.memory += allocation.memory; + return totals; + }, + { cpu: 0, memory: 0 } + ); + } + + @action + async loadAllocations() { + await RSVP.all(this.args.nodes.mapBy('allocations')); + + this.scheduledAllocations = this.args.nodes.reduce( + (all, node) => all.concat(node.allocations.filterBy('isScheduled')), + [] + ); + + this.aggregatedNodeResources = this.args.nodes.mapBy('resources').reduce( + (totals, node) => { + totals.cpu += node.cpu; + totals.memory += node.memory; + return totals; + }, + { cpu: 0, memory: 0 } + ); + + this.isLoaded = true; + } +} diff --git a/ui/app/components/topo-viz/node.js b/ui/app/components/topo-viz/node.js index 3e28f1304..7d8b6bd8b 100644 --- a/ui/app/components/topo-viz/node.js +++ b/ui/app/components/topo-viz/node.js @@ -1,7 +1,124 @@ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { guidFor } from '@ember/object/internals'; + +export default class TopoVizNode extends Component { + @tracked data = { cpu: [], memory: [] }; + // @tracked height = 15; + @tracked dimensionsWidth = 0; + @tracked padding = 5; + @tracked activeAllocation = null; + + get height() { + return this.args.heightScale ? this.args.heightScale(this.args.node.resources.memory) : 15; + } + + get yOffset() { + return this.height + 2; + } + + get maskHeight() { + return this.height + this.yOffset; + } + + get totalHeight() { + return this.maskHeight + this.padding * 2; + } + + get maskId() { + return `topo-viz-node-mask-${guidFor(this)}`; + } -export default class TopoViz extends Component { get count() { return this.args.node.get('allocations.length'); } + + get allocations() { + return this.args.node.allocations.filterBy('isScheduled').sortBy('resources.memory'); + } + + @action + async reloadNode() { + if (this.args.node.isPartial) { + await this.args.node.reload(); + this.data = this.computeData(this.dimensionsWidth); + } + } + + @action + render(svg) { + this.dimensionsWidth = svg.clientWidth - this.padding * 2; + this.data = this.computeData(this.dimensionsWidth); + } + + @action + highlightAllocation(allocation) { + this.activeAllocation = allocation; + } + + @action + clearHighlight() { + this.activeAllocation = null; + } + + computeData(width) { + // TODO: differentiate reserved and resources + if (!this.args.node.resources) return; + + const totalCPU = this.args.node.resources.cpu; + const totalMemory = this.args.node.resources.memory; + let cpuOffset = 0; + let memoryOffset = 0; + + const cpu = []; + const memory = []; + for (const allocation of this.allocations) { + const cpuPercent = allocation.resources.cpu / totalCPU; + const memoryPercent = allocation.resources.memory / totalMemory; + const isFirst = allocation === this.allocations[0]; + + let cpuWidth = cpuPercent * width - 1; + let memoryWidth = memoryPercent * width - 1; + if (isFirst) { + cpuWidth += 0.5; + memoryWidth += 0.5; + } + + cpu.push({ + allocation, + offset: cpuOffset * 100, + percent: cpuPercent * 100, + width: cpuWidth, + x: cpuOffset * width + (isFirst ? 0 : 0.5), + className: allocation.clientStatus, + }); + memory.push({ + allocation, + offset: memoryOffset * 100, + percent: memoryPercent * 100, + width: memoryWidth, + x: memoryOffset * width + (isFirst ? 0 : 0.5), + className: allocation.clientStatus, + }); + + cpuOffset += cpuPercent; + memoryOffset += memoryPercent; + } + + const cpuRemainder = { + x: cpuOffset * width + 0.5, + width: width - cpuOffset * width, + }; + const memoryRemainder = { + x: memoryOffset * width + 0.5, + width: width - memoryOffset * width, + }; + + return { cpu, memory, cpuRemainder, memoryRemainder }; + } } + +// capture width on did insert element +// update width on window resize +// recompute data when width changes diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index a9a60d351..3aa09fa1c 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -47,6 +47,11 @@ export default class Allocation extends Model { @equal('clientStatus', 'running') isRunning; @attr('boolean') isMigrating; + @computed('clientStatus') + get isScheduled() { + return ['pending', 'running', 'failed'].includes(this.clientStatus); + } + // An allocation model created from any allocation list response will be lacking // many properties (some of which can always be null). This is an indicator that // the allocation needs to be reloaded to get the complete allocation state. diff --git a/ui/app/styles/charts.scss b/ui/app/styles/charts.scss index bdb259dd2..cfa14ddbb 100644 --- a/ui/app/styles/charts.scss +++ b/ui/app/styles/charts.scss @@ -4,6 +4,7 @@ @import './charts/tooltip'; @import './charts/colors'; @import './charts/chart-annotation.scss'; +@import './charts/topo-viz-node.scss'; .inline-chart { height: 1.5rem; diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss new file mode 100644 index 000000000..f0dadf106 --- /dev/null +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -0,0 +1,46 @@ +.chart.topo-viz-node { + display: block; + + svg { + display: inline-block; + height: 100%; + width: 100%; + overflow: visible; + + .node-background { + fill: $white-ter; + stroke-width: 1; + stroke: $grey-lighter; + } + + .dimension-background { + fill: lighten($grey-lighter, 5%); + } + + .dimensions.is-active { + .bar { + opacity: 0.2; + + &.is-active { + opacity: 1; + } + } + } + } + + & + .topo-viz-node { + margin-top: 1em; + } + + &.is-empty { + .node-background { + stroke: $red; + stroke-width: 2; + fill: $white; + } + + .dimension-background { + fill: none; + } + } +} diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 40860cb0b..03e1a96c7 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,5 +1,7 @@ -
- {{#each this.datacenters as |dc|}} - - {{/each}} +
+ {{#if this.isLoaded}} + {{#each this.datacenters as |dc|}} + + {{/each}} + {{/if}}
diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index 60aa94605..532425716 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -1,8 +1,16 @@ -
-
{{@datacenter}}
+
+
+ {{@datacenter}} + {{this.scheduledAllocations.length}} Allocs, + {{@nodes.length}} Nodes, + {{this.aggregatedAllocationResources.memory}}/{{this.aggregatedNodeResources.memory}} MiB, + {{this.aggregatedAllocationResources.cpu}}/{{this.aggregatedNodeResources.cpu}} Mhz +
- {{#each @nodes as |node|}} - - {{/each}} + {{#if this.isLoaded}} + {{#each @nodes as |node|}} + + {{/each}} + {{/if}}
diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 0e0581013..819a58401 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,11 +1,61 @@ -
- Node! {{@node.name}} ({{this.count}} allocs) ({{@node.resources.memory}} MiB, {{@node.resources.cpu}} Mhz) -
    - {{#each @node.allocations as |allocation|}} -
  • - {{allocation.name}} {{allocation.shortId}} - ({{allocation.resources.memory}} MiB, {{allocation.resources.cpu}} Mhz) -
  • - {{/each}} -
+
+

{{@node.name}} ({{this.count}} allocs) ({{@node.resources.memory}} MiB, {{@node.resources.cpu}} Mhz) (% reserved would be nice)

+ + + + + + + + + + {{#if this.data.memoryRemainder}} + + {{/if}} + {{#each this.data.memory as |memory|}} + + + {{#if (or (eq memory.className "starting") (eq memory.className "pending"))}} + + {{/if}} + + {{/each}} + + + {{#if this.data.cpuRemainder}} + + {{/if}} + {{#each this.data.cpu as |cpu|}} + + + {{#if (or (eq cpu.className "starting") (eq cpu.className "pending"))}} + + {{/if}} + + {{/each}} + + + +
+ {{this.activeAllocation.name}} {{this.activeAllocation.shortId}} + ({{this.activeAllocation.resources.memory}} MiB, {{this.activeAllocation.resources.cpu}} Mhz) +
+ From 78ae8fd78b194040435bfe4b23f3e66342407af7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 Sep 2020 00:44:49 -0700 Subject: [PATCH 07/52] Fix factory bug that made it so pending allocs had no resources --- ui/mirage/factories/allocation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index c571271d0..c901a08ca 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -206,7 +206,7 @@ export default Factory.extend({ allocation.update({ taskStateIds: allocation.clientStatus === 'pending' ? [] : states.mapBy('id'), - taskResourceIds: allocation.clientStatus === 'pending' ? [] : resources.mapBy('id'), + taskResourceIds: resources.mapBy('id'), }); // Each allocation has a corresponding allocation stats running on some client. From 000c00e9204b474259ddd587cc3dfde306f5a07d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 Sep 2020 12:53:18 -0700 Subject: [PATCH 08/52] Start click interaction for topo viz allocs --- ui/app/components/topo-viz.js | 9 ++++++++- ui/app/components/topo-viz/node.js | 5 +++++ ui/app/templates/components/topo-viz.hbs | 2 +- ui/app/templates/components/topo-viz/datacenter.hbs | 2 +- ui/app/templates/components/topo-viz/node.hbs | 10 ++++++++-- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 0e6d7b032..f1882ef49 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -9,6 +9,8 @@ export default class TopoViz extends Component { @tracked heightScale = null; @tracked isLoaded = false; + @tracked activeTaskGroup = null; + get datacenters() { const datacentersMap = this.args.nodes.reduce((datacenters, node) => { if (!datacenters[node.datacenter]) datacenters[node.datacenter] = []; @@ -27,8 +29,13 @@ export default class TopoViz extends Component { // TODO: Make the range dynamic based on the extent of the domain this.heightScale = scaleLinear() - .range([15, 50]) + .range([15, 30]) .domain([0, max(this.args.nodes.map(node => node.resources.memory))]); this.isLoaded = true; } + + @action + associateAllocations(allocation) { + this.activeTaskGroup = allocation.taskGroup; + } } diff --git a/ui/app/components/topo-viz/node.js b/ui/app/components/topo-viz/node.js index 7d8b6bd8b..5ffb658a0 100644 --- a/ui/app/components/topo-viz/node.js +++ b/ui/app/components/topo-viz/node.js @@ -62,6 +62,11 @@ export default class TopoVizNode extends Component { this.activeAllocation = null; } + @action + selectAllocation(allocation) { + if (this.args.onAllocationSelect) this.args.onAllocationSelect(allocation); + } + computeData(width) { // TODO: differentiate reserved and resources if (!this.args.node.resources) return; diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 03e1a96c7..acf18f82d 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,7 +1,7 @@
{{#if this.isLoaded}} {{#each this.datacenters as |dc|}} - + {{/each}} {{/if}}
diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index 532425716..e54b19d19 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -9,7 +9,7 @@
{{#if this.isLoaded}} {{#each @nodes as |node|}} - + {{/each}} {{/if}}
diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 819a58401..768986f18 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -25,7 +25,10 @@ height="{{this.height}}px" /> {{/if}} {{#each this.data.memory as |memory|}} - + {{#if (or (eq memory.className "starting") (eq memory.className "pending"))}} @@ -43,7 +46,10 @@ height="{{this.height}}px" /> {{/if}} {{#each this.data.cpu as |cpu|}} - + {{#if (or (eq cpu.className "starting") (eq cpu.className "pending"))}} From 49736aed46a66ebfc532b5c6c90c1de4a90eba48 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 Sep 2020 12:53:28 -0700 Subject: [PATCH 09/52] Tweak topo scenario --- ui/mirage/scenarios/topo.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/mirage/scenarios/topo.js b/ui/mirage/scenarios/topo.js index d22763ff8..474ea361e 100644 --- a/ui/mirage/scenarios/topo.js +++ b/ui/mirage/scenarios/topo.js @@ -7,8 +7,8 @@ export function topoSmall(server) { datacenter: 'dc1', status: 'ready', resources: { - CPU: 4000, - MemoryMB: 8192, + CPU: 3000, + MemoryMB: 5192, DiskMB: 10000, IOPS: 100000, Networks: generateNetworks(), @@ -39,9 +39,10 @@ export function topoSmall(server) { }); }); - server.createList('allocation', 35, { + server.createList('allocation', 25, { forceRunningClientStatus: true, }); + //server.createList('allocation', 15, { clientStatus: 'pending' }); } export function topoSmallProblems(server) {} From 831d27efd638b0e643503cd21d9fa8cc7aa9544f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 10 Sep 2020 19:29:25 -0700 Subject: [PATCH 10/52] Fleshing out the first prototype of the topology visualization --- ui/app/components/topo-viz.js | 11 +++- ui/app/components/topo-viz/node.js | 58 +++++++++++++++++-- ui/app/styles/charts/colors.scss | 4 ++ ui/app/styles/charts/topo-viz-node.scss | 13 +++++ ui/app/styles/components.scss | 1 + ui/app/styles/components/legend.scss | 27 +++++++++ ui/app/styles/core/variables.scss | 1 + ui/app/templates/components/topo-viz.hbs | 8 ++- .../components/topo-viz/datacenter.hbs | 7 ++- ui/app/templates/components/topo-viz/node.hbs | 47 +++++++++++---- ui/app/templates/topology.hbs | 36 ++++++++++-- 11 files changed, 187 insertions(+), 26 deletions(-) create mode 100644 ui/app/styles/components/legend.scss diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index f1882ef49..665439f1b 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -10,6 +10,7 @@ export default class TopoViz extends Component { @tracked isLoaded = false; @tracked activeTaskGroup = null; + @tracked activeJobId = null; get datacenters() { const datacentersMap = this.args.nodes.reduce((datacenters, node) => { @@ -36,6 +37,14 @@ export default class TopoViz extends Component { @action associateAllocations(allocation) { - this.activeTaskGroup = allocation.taskGroup; + const taskGroup = allocation.taskGroupName; + const jobId = allocation.belongsTo('job').id(); + if (this.activeTaskGroup === taskGroup && this.activeJobId === jobId) { + this.activeTaskGroup = null; + this.activeJobId = null; + } else { + this.activeTaskGroup = taskGroup; + this.activeJobId = jobId; + } } } diff --git a/ui/app/components/topo-viz/node.js b/ui/app/components/topo-viz/node.js index 5ffb658a0..c947cdbd4 100644 --- a/ui/app/components/topo-viz/node.js +++ b/ui/app/components/topo-viz/node.js @@ -5,7 +5,6 @@ import { guidFor } from '@ember/object/internals'; export default class TopoVizNode extends Component { @tracked data = { cpu: [], memory: [] }; - // @tracked height = 15; @tracked dimensionsWidth = 0; @tracked padding = 5; @tracked activeAllocation = null; @@ -14,6 +13,27 @@ export default class TopoVizNode extends Component { return this.args.heightScale ? this.args.heightScale(this.args.node.resources.memory) : 15; } + get labelHeight() { + return this.height / 2; + } + + get paddingLeft() { + const labelWidth = 20; + return this.padding + labelWidth; + } + + // Since strokes are placed centered on the perimeter of fills, The width of the stroke needs to be removed from + // the height of the fill to match unstroked height and avoid clipping. + get selectedHeight() { + return this.height - 1; + } + + // Since strokes are placed centered on the perimeter of fills, half the width of the stroke needs to be added to + // the yOffset to match heights with unstroked shapes. + get selectedYOffset() { + return this.height + 2.5; + } + get yOffset() { return this.height + 2; } @@ -35,7 +55,17 @@ export default class TopoVizNode extends Component { } get allocations() { - return this.args.node.allocations.filterBy('isScheduled').sortBy('resources.memory'); + // return this.args.node.allocations.filterBy('isScheduled').sortBy('resources.memory'); + const totalCPU = this.args.node.resources.cpu; + const totalMemory = this.args.node.resources.memory; + + // Sort by the delta between memory and cpu percent. This creates the least amount of + // drift between the positional alignment of an alloc's cpu and memory representations. + return this.args.node.allocations.filterBy('isScheduled').sort((a, b) => { + const deltaA = Math.abs(a.resources.memory / totalMemory - a.resources.cpu / totalCPU); + const deltaB = Math.abs(b.resources.memory / totalMemory - b.resources.cpu / totalCPU); + return deltaA - deltaB; + }); } @action @@ -48,7 +78,7 @@ export default class TopoVizNode extends Component { @action render(svg) { - this.dimensionsWidth = svg.clientWidth - this.padding * 2; + this.dimensionsWidth = svg.clientWidth - this.padding - this.paddingLeft; this.data = this.computeData(this.dimensionsWidth); } @@ -82,6 +112,9 @@ export default class TopoVizNode extends Component { const cpuPercent = allocation.resources.cpu / totalCPU; const memoryPercent = allocation.resources.memory / totalMemory; const isFirst = allocation === this.allocations[0]; + const isSelected = + allocation.taskGroupName === this.args.activeTaskGroup && + allocation.belongsTo('job').id() === this.args.activeJobId; let cpuWidth = cpuPercent * width - 1; let memoryWidth = memoryPercent * width - 1; @@ -89,21 +122,27 @@ export default class TopoVizNode extends Component { cpuWidth += 0.5; memoryWidth += 0.5; } + if (isSelected) { + cpuWidth--; + memoryWidth--; + } cpu.push({ allocation, + isSelected, offset: cpuOffset * 100, percent: cpuPercent * 100, width: cpuWidth, - x: cpuOffset * width + (isFirst ? 0 : 0.5), + x: cpuOffset * width + (isFirst ? 0 : 0.5) + (isSelected ? 0.5 : 0), className: allocation.clientStatus, }); memory.push({ allocation, + isSelected, offset: memoryOffset * 100, percent: memoryPercent * 100, width: memoryWidth, - x: memoryOffset * width + (isFirst ? 0 : 0.5), + x: memoryOffset * width + (isFirst ? 0 : 0.5) + (isSelected ? 0.5 : 0), className: allocation.clientStatus, }); @@ -120,7 +159,14 @@ export default class TopoVizNode extends Component { width: width - memoryOffset * width, }; - return { cpu, memory, cpuRemainder, memoryRemainder }; + return { + cpu, + memory, + cpuRemainder, + memoryRemainder, + cpuLabel: { x: -this.paddingLeft / 2, y: this.height / 2 + this.yOffset }, + memoryLabel: { x: -this.paddingLeft / 2, y: this.height / 2 }, + }; } } diff --git a/ui/app/styles/charts/colors.scss b/ui/app/styles/charts/colors.scss index 6eedeeaec..dce0a3ddf 100644 --- a/ui/app/styles/charts/colors.scss +++ b/ui/app/styles/charts/colors.scss @@ -47,6 +47,10 @@ $lost: $dark; vertical-align: middle; border-radius: $radius; + &.is-wide { + width: 2rem; + } + $color-sequence: $orange, $yellow, $green, $turquoise, $blue, $purple, $red; @for $i from 1 through length($color-sequence) { &.swatch-#{$i - 1} { diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index f0dadf106..906cbb1a0 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -26,6 +26,19 @@ } } } + + .bar.is-selected { + stroke-width: 1px; + stroke: $blue; + fill: $blue-light; + } + + .label { + text-anchor: middle; + alignment-baseline: central; + font-weight: $weight-normal; + fill: $grey; + } } & + .topo-viz-node { diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index 1f1058f36..d00d53d0b 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -20,6 +20,7 @@ @import './components/inline-definitions'; @import './components/job-diff'; @import './components/json-viewer'; +@import './components/legend'; @import './components/lifecycle-chart'; @import './components/loading-spinner'; @import './components/metrics'; diff --git a/ui/app/styles/components/legend.scss b/ui/app/styles/components/legend.scss new file mode 100644 index 000000000..a1ce69d7f --- /dev/null +++ b/ui/app/styles/components/legend.scss @@ -0,0 +1,27 @@ +.legend { + margin-bottom: 1em; + + .legend-label { + font-weight: $weight-bold; + margin-bottom: 0.3em; + } + + .legend-terms { + dt, + dd { + display: inline; + } + + dt { + font-weight: $weight-bold; + } + + dd { + margin-left: 0.5em; + } + + dd + dt { + margin-left: 1.5em; + } + } +} diff --git a/ui/app/styles/core/variables.scss b/ui/app/styles/core/variables.scss index cee22b48f..24832cf1a 100644 --- a/ui/app/styles/core/variables.scss +++ b/ui/app/styles/core/variables.scss @@ -4,6 +4,7 @@ $blue: $vagrant-blue; $purple: $terraform-purple; $red: #c84034; $grey-blue: #bbc4d1; +$blue-light: #c0d5ff; $primary: $nomad-green; $warning: $orange; diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index acf18f82d..39a6410ba 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,7 +1,13 @@
{{#if this.isLoaded}} {{#each this.datacenters as |dc|}} - + {{/each}} {{/if}}
diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index e54b19d19..90dab4312 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -9,7 +9,12 @@
{{#if this.isLoaded}} {{#each @nodes as |node|}} - + {{/each}} {{/if}}
diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 768986f18..7ee5eb359 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,6 +1,6 @@

{{@node.name}} ({{this.count}} allocs) ({{@node.resources.memory}} MiB, {{@node.resources.cpu}} Mhz) (% reserved would be nice)

- + @@ -9,14 +9,14 @@ + M {{#if this.data.memoryRemainder}} {{/if}} {{#each this.data.memory as |memory|}} - - + + {{#if (or (eq memory.className "starting") (eq memory.className "pending"))}} - + {{/if}} {{/each}} + C {{#if this.data.cpuRemainder}} - + {{#if (or (eq cpu.className "starting") (eq cpu.className "pending"))}} - + {{/if}} {{/each}} diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index a501d89d3..79d37b172 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -1,12 +1,38 @@ {{title "Cluster Topology"}}
-
-
Cluster Metrics
-
- Aggregate metrics go here. +
+
+
+
Legend
+
+
+

Metrics

+
+
M:
Memory
+
C:
CPU
+
+
+
+

Allocation Status

+
+
Running
+
Failed
+
Starting
+
+
+
+
+
+
Cluster Details
+
+ Aggregate metrics go here. +
+
+
+
+
- From 87a902db0c90788a12e66a12f152862eaac93a34 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 10 Sep 2020 21:17:09 -0700 Subject: [PATCH 11/52] Touch up topo viz interactions --- ui/app/styles/charts/topo-viz-node.scss | 12 +- ui/app/templates/components/topo-viz/node.hbs | 124 +++++++++--------- 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index 906cbb1a0..6deec9d99 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -27,10 +27,14 @@ } } - .bar.is-selected { - stroke-width: 1px; - stroke: $blue; - fill: $blue-light; + .bar { + cursor: pointer; + + &.is-selected { + stroke-width: 1px; + stroke: $blue; + fill: $blue-light; + } } .label { diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 7ee5eb359..c7ac58c46 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -7,80 +7,82 @@ - - - M - {{#if this.data.memoryRemainder}} - - {{/if}} - {{#each this.data.memory as |memory|}} - + {{#if this.allocations.length}} + + + M + {{#if this.data.memoryRemainder}} - {{#if (or (eq memory.className "starting") (eq memory.className "pending"))}} + class="dimension-background" + x="{{this.data.memoryRemainder.x}}px" + width="{{this.data.memoryRemainder.width}}px" + height="{{this.height}}px" /> + {{/if}} + {{#each this.data.memory as |memory|}} + - {{/if}} - - {{/each}} - - - C - {{#if this.data.cpuRemainder}} - - {{/if}} - {{#each this.data.cpu as |cpu|}} - + class="layer-0" /> + {{#if (or (eq memory.className "starting") (eq memory.className "pending"))}} + + {{/if}} + + {{/each}} + + + C + {{#if this.data.cpuRemainder}} - {{#if (or (eq cpu.className "starting") (eq cpu.className "pending"))}} + class="dimension-background" + x="{{this.data.cpuRemainder.x}}px" + y="{{this.yOffset}}px" + width="{{this.data.cpuRemainder.width}}px" + height="{{this.height}}px" /> + {{/if}} + {{#each this.data.cpu as |cpu|}} + - {{/if}} - - {{/each}} + class="layer-0" /> + {{#if (or (eq cpu.className "starting") (eq cpu.className "pending"))}} + + {{/if}} + + {{/each}} + - + {{/if}}
{{this.activeAllocation.name}} {{this.activeAllocation.shortId}} From cab4e618fce9e70ad1b3e7f3fbc42e300a6f1497 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 10 Sep 2020 23:29:07 -0700 Subject: [PATCH 12/52] Add cluster details to the topology page --- ui/app/components/topo-viz.js | 2 + ui/app/controllers/topology.js | 57 ++++++++++++++++ ui/app/helpers/format-bytes.js | 11 +++- ui/app/styles/components.scss | 1 + .../styles/components/dashboard-metric.scss | 31 +++++++++ ui/app/templates/topology.hbs | 66 ++++++++++++++++++- ui/mirage/scenarios/topo.js | 2 +- 7 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 ui/app/controllers/topology.js create mode 100644 ui/app/styles/components/dashboard-metric.scss diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 665439f1b..3332983de 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -42,9 +42,11 @@ export default class TopoViz extends Component { if (this.activeTaskGroup === taskGroup && this.activeJobId === jobId) { this.activeTaskGroup = null; this.activeJobId = null; + if (this.args.onAllocationSelect) this.args.onAllocationSelect(null); } else { this.activeTaskGroup = taskGroup; this.activeJobId = jobId; + if (this.args.onAllocationSelect) this.args.onAllocationSelect(allocation); } } } diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js new file mode 100644 index 000000000..50ea067ca --- /dev/null +++ b/ui/app/controllers/topology.js @@ -0,0 +1,57 @@ +import Controller from '@ember/controller'; +import { computed } from '@ember/object'; +import classic from 'ember-classic-decorator'; +import { reduceToLargestUnit } from 'nomad-ui/helpers/format-bytes'; + +@classic +export default class TopologyControllers extends Controller { + get datacenters() { + return Array.from(new Set(this.model.nodes.mapBy('datacenter'))).compact(); + } + + @computed('model.nodes.@each.resources') + get totalMemory() { + const mibs = this.model.nodes + .mapBy('resources.memory') + .reduce((sum, memory) => sum + (memory || 0), 0); + return mibs * 1024 * 1024; + } + + @computed('model.nodes.@each.resources') + get totalCPU() { + return this.model.nodes.mapBy('resources.cpu').reduce((sum, cpu) => sum + (cpu || 0), 0); + } + + @computed('totalMemory') + get totalMemoryFormatted() { + return reduceToLargestUnit(this.totalMemory)[0].toFixed(2); + } + + @computed('totalCPU') + get totalMemoryUnits() { + return reduceToLargestUnit(this.totalMemory)[1]; + } + + @computed('model.allocations.@each.resources') + get totalReservedMemory() { + const mibs = this.model.allocations + .mapBy('resources.memory') + .reduce((sum, memory) => sum + (memory || 0), 0); + return mibs * 1024 * 1024; + } + + @computed('model.allocations.@each.resources') + get totalReservedCPU() { + return this.model.allocations.mapBy('resources.cpu').reduce((sum, cpu) => sum + (cpu || 0), 0); + } + + @computed('totalMemory', 'totalReservedMemory') + get reservedMemoryPercent() { + return this.totalReservedMemory / this.totalMemory; + } + + @computed('totalCPU', 'totalReservedCPU') + get reservedCPUPercent() { + return this.totalReservedCPU / this.totalCPU; + } +} diff --git a/ui/app/helpers/format-bytes.js b/ui/app/helpers/format-bytes.js index b2c69ed06..ba204fbfc 100644 --- a/ui/app/helpers/format-bytes.js +++ b/ui/app/helpers/format-bytes.js @@ -1,6 +1,6 @@ import Helper from '@ember/component/helper'; -const UNITS = ['Bytes', 'KiB', 'MiB']; +const UNITS = ['Bytes', 'KiB', 'MiB', 'GiB']; /** * Bytes Formatter @@ -10,7 +10,7 @@ const UNITS = ['Bytes', 'KiB', 'MiB']; * Outputs the bytes reduced to the largest supported unit size for which * bytes is larger than one. */ -export function formatBytes([bytes]) { +export function reduceToLargestUnit(bytes) { bytes || (bytes = 0); let unitIndex = 0; while (bytes >= 1024 && unitIndex < UNITS.length - 1) { @@ -18,7 +18,12 @@ export function formatBytes([bytes]) { unitIndex++; } - return `${Math.floor(bytes)} ${UNITS[unitIndex]}`; + return [bytes, UNITS[unitIndex]]; +} + +export function formatBytes([bytes]) { + const [number, unit] = reduceToLargestUnit(bytes); + return `${Math.floor(number)} ${unit}`; } export default Helper.helper(formatBytes); diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index d00d53d0b..40b4e1f7a 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -4,6 +4,7 @@ @import './components/codemirror'; @import './components/copy-button'; @import './components/cli-window'; +@import './components/dashboard-metric'; @import './components/dropdown'; @import './components/ember-power-select'; @import './components/empty-message'; diff --git a/ui/app/styles/components/dashboard-metric.scss b/ui/app/styles/components/dashboard-metric.scss new file mode 100644 index 000000000..3b6cfb8cb --- /dev/null +++ b/ui/app/styles/components/dashboard-metric.scss @@ -0,0 +1,31 @@ +.dashboard-metric { + margin-top: 1.5em; + + .metric { + text-align: left; + font-weight: $weight-bold; + font-size: $size-3; + + .metric-units { + font-size: $size-4; + } + + .metric-label { + font-size: $body-size; + font-weight: $weight-normal; + } + } + + .graphic { + padding-bottom: 0; + margin-bottom: 0; + + > .column { + padding: 0.5rem 0.75rem; + } + } + + .annotation { + margin-top: -0.75rem; + } +} diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index 79d37b172..26abab726 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -24,14 +24,74 @@
-
Cluster Details
+
{{if this.activeAllocation "Allocation" "Cluster"}} Details
- Aggregate metrics go here. + {{#if this.activeAllocation}} + Showing alloc details for {{this.activeAllocation.shortId}} + {{else}} +
+
+

DCs

+

{{this.datacenters.length}}

+
+
+

Clients

+

{{this.model.nodes.length}}

+
+
+

Allocations

+ {{! TODO: make sure that this is only the scheduled allocations }} +

{{this.model.allocations.length}}

+
+
+
+

{{this.totalMemoryFormatted}} {{this.totalMemoryUnits}} of memory

+
+
+
+ + {{this.reservedMemoryPercent}} + +
+
+
+ {{format-percentage this.reservedMemoryPercent total=1}} +
+
+
+ {{format-bytes this.totalReservedMemory}} / {{format-bytes this.totalMemory}} reserved +
+
+
+

{{this.totalCPU}} Mhz of CPU

+
+
+
+ + {{this.reservedCPUPercent}} + +
+
+
+ {{format-percentage this.reservedCPUPercent total=1}} +
+
+
+ {{this.totalReservedCPU}} Mhz / {{this.totalCPU}} Mhz reserved +
+
+ {{/if}}
- +
diff --git a/ui/mirage/scenarios/topo.js b/ui/mirage/scenarios/topo.js index 474ea361e..e75bf7fa5 100644 --- a/ui/mirage/scenarios/topo.js +++ b/ui/mirage/scenarios/topo.js @@ -17,7 +17,7 @@ export function topoSmall(server) { }); const jobResources = [ - ['M: 256, C: 150'], + ['M: 2560, C: 150'], ['M: 128, C: 400'], ['M: 512, C: 100'], ['M: 256, C: 150'], From e4907dc647ddc59329d2a11cda749807ed027374 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 10 Sep 2020 23:59:58 -0700 Subject: [PATCH 13/52] Super rough allocation details, needs some style love --- ui/app/controllers/topology.js | 26 ++++++++++++++++++- ui/app/styles/components/primary-metric.scss | 4 +++ ui/app/templates/topology.hbs | 27 ++++++++++++++++++-- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index 50ea067ca..5f70ef2bd 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -1,5 +1,5 @@ import Controller from '@ember/controller'; -import { computed } from '@ember/object'; +import { computed, action } from '@ember/object'; import classic from 'ember-classic-decorator'; import { reduceToLargestUnit } from 'nomad-ui/helpers/format-bytes'; @@ -54,4 +54,28 @@ export default class TopologyControllers extends Controller { get reservedCPUPercent() { return this.totalReservedCPU / this.totalCPU; } + + @computed('activeAllocation', 'model.allocations.@each.{taskGroupName,job}') + get siblingAllocations() { + if (!this.activeAllocation) return []; + const taskGroup = this.activeAllocation.taskGroupName; + const jobId = this.activeAllocation.belongsTo('job').id(); + + return this.model.allocations.filter(allocation => { + return allocation.taskGroupName === taskGroup && allocation.belongsTo('job').id() === jobId; + }); + } + + @computed('siblingAllocations.@each.node') + get uniqueActiveAllocationNodes() { + return this.siblingAllocations.mapBy('node').uniq(); + } + + @action + setAllocation(allocation) { + this.set('activeAllocation', allocation); + if (allocation) { + allocation.reload(); + } + } } diff --git a/ui/app/styles/components/primary-metric.scss b/ui/app/styles/components/primary-metric.scss index e48b52d16..ba3777767 100644 --- a/ui/app/styles/components/primary-metric.scss +++ b/ui/app/styles/components/primary-metric.scss @@ -13,6 +13,10 @@ height: 150px; } + &.is-short .primary-graphic { + height: 100px; + } + .secondary-graphic { padding: 0.75em; padding-bottom: 0; diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index 26abab726..d8dc5044b 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -27,7 +27,30 @@
{{if this.activeAllocation "Allocation" "Cluster"}} Details
{{#if this.activeAllocation}} - Showing alloc details for {{this.activeAllocation.shortId}} +

+ Allocation: + {{this.activeAllocation.shortId}} +

+

Sibling Allocations: {{this.siblingAllocations.length}}

+

Unique Client Placements: {{this.uniqueActiveAllocationNodes.length}}

+

+

+ Job: + + {{this.activeAllocation.job.name}} + +

+

+ Client: + + {{this.activeAllocation.node.name}} ({{this.activeAllocation.node.shortId}}) + +

+ + {{else}}
@@ -91,7 +114,7 @@
- +
From 5bc4d1f1d55409526740b72dee48f65740a0cb2d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Sep 2020 00:56:14 -0700 Subject: [PATCH 14/52] Associate sibling allocations by drawing lines --- ui/app/components/topo-viz.js | 68 ++++++++++++++++--- ui/app/styles/charts/topo-viz-node.scss | 15 ++++ ui/app/templates/components/topo-viz.hbs | 10 ++- ui/app/templates/components/topo-viz/node.hbs | 10 ++- 4 files changed, 89 insertions(+), 14 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 3332983de..0acac3a19 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -1,6 +1,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +import { run } from '@ember/runloop'; import { scaleLinear } from 'd3-scale'; import { max } from 'd3-array'; import RSVP from 'rsvp'; @@ -8,9 +9,18 @@ import RSVP from 'rsvp'; export default class TopoViz extends Component { @tracked heightScale = null; @tracked isLoaded = false; + @tracked element = null; - @tracked activeTaskGroup = null; - @tracked activeJobId = null; + @tracked activeAllocation = null; + @tracked activeEdges = []; + + get activeTaskGroup() { + return this.activeAllocation && this.activeAllocation.taskGroupName; + } + + get activeJobId() { + return this.activeAllocation && this.activeAllocation.belongsTo('job').id(); + } get datacenters() { const datacentersMap = this.args.nodes.reduce((datacenters, node) => { @@ -35,18 +45,54 @@ export default class TopoViz extends Component { this.isLoaded = true; } + @action + captureElement(element) { + this.element = element; + } + @action associateAllocations(allocation) { - const taskGroup = allocation.taskGroupName; - const jobId = allocation.belongsTo('job').id(); - if (this.activeTaskGroup === taskGroup && this.activeJobId === jobId) { - this.activeTaskGroup = null; - this.activeJobId = null; - if (this.args.onAllocationSelect) this.args.onAllocationSelect(null); + if (this.activeAllocation === allocation) { + this.activeAllocation = null; + this.activeEdges = []; } else { - this.activeTaskGroup = taskGroup; - this.activeJobId = jobId; - if (this.args.onAllocationSelect) this.args.onAllocationSelect(allocation); + this.activeAllocation = allocation; + this.computedActiveEdges(); } + if (this.args.onAllocationSelect) this.args.onAllocationSelect(this.activeAllocation); + } + + computedActiveEdges() { + // Wait a render cycle + run.next(() => { + const activeEl = this.element.querySelector( + `[data-allocation-id="${this.activeAllocation.id}"]` + ); + const selectedAllocations = this.element.querySelectorAll('.memory .bar.is-selected'); + const activeBBox = activeEl.getBoundingClientRect(); + console.log('bb', activeBBox); + console.log('win', window.visualViewport); + + const vLeft = window.visualViewport.pageLeft; + const vTop = window.visualViewport.pageTop; + + const edges = []; + for (let allocation of selectedAllocations) { + if (allocation !== activeEl) { + const bbox = allocation.getBoundingClientRect(); + edges.push({ + x1: activeBBox.x + activeBBox.width / 2 + vLeft, + y1: activeBBox.y + activeBBox.height / 2 + vTop, + x2: bbox.x + bbox.width / 2 + vLeft, + y2: bbox.y + bbox.height / 2 + vTop, + }); + } + } + + this.activeEdges = edges; + }); + // get element for active alloc + // get element for all selected allocs + // draw lines between centroid of each } } diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index 6deec9d99..e0006a614 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -61,3 +61,18 @@ } } } + +.chart.topo-viz-edges { + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + pointer-events: none; + overflow: visible; + + .edge { + stroke-width: 2; + stroke: $blue; + } +} diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 39a6410ba..7f641443b 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,4 +1,4 @@ -
+
{{#if this.isLoaded}} {{#each this.datacenters as |dc|}} {{/each}} + + {{#if this.activeAllocation}} + + {{#each this.activeEdges as |edge|}} + + {{/each}} + + {{/if}} {{/if}}
diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index c7ac58c46..849849197 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -17,7 +17,9 @@ {{on "mouseout" this.clearHighlight}} > - M + {{#if this.data.memoryLabel}} + M + {{/if}} {{#if this.data.memoryRemainder}} - C + {{#if this.data.cpuLabel}} + C + {{/if}} {{#if this.data.cpuRemainder}} Date: Fri, 11 Sep 2020 12:15:41 -0700 Subject: [PATCH 15/52] Guard against undefined denominators --- ui/app/components/topo-viz.js | 3 +-- ui/app/controllers/topology.js | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 0acac3a19..59186b27f 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -62,6 +62,7 @@ export default class TopoViz extends Component { if (this.args.onAllocationSelect) this.args.onAllocationSelect(this.activeAllocation); } + @action computedActiveEdges() { // Wait a render cycle run.next(() => { @@ -70,8 +71,6 @@ export default class TopoViz extends Component { ); const selectedAllocations = this.element.querySelectorAll('.memory .bar.is-selected'); const activeBBox = activeEl.getBoundingClientRect(); - console.log('bb', activeBBox); - console.log('win', window.visualViewport); const vLeft = window.visualViewport.pageLeft; const vTop = window.visualViewport.pageTop; diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index 5f70ef2bd..200132e40 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -47,11 +47,13 @@ export default class TopologyControllers extends Controller { @computed('totalMemory', 'totalReservedMemory') get reservedMemoryPercent() { + if (!this.totalReservedMemory || !this.totalMemory) return 0; return this.totalReservedMemory / this.totalMemory; } @computed('totalCPU', 'totalReservedCPU') get reservedCPUPercent() { + if (!this.totalReservedCPU || !this.totalCPU) return 0; return this.totalReservedCPU / this.totalCPU; } From fb1deb5c4021f7a706e1c5d6920e65148b161403 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Sep 2020 14:53:18 -0700 Subject: [PATCH 16/52] Updated alloc and cluster details on topo page --- .../styles/components/dashboard-metric.scss | 17 +++- ui/app/styles/core/columns.scss | 4 + ui/app/templates/components/topo-viz.hbs | 2 +- ui/app/templates/topology.hbs | 85 ++++++++++--------- 4 files changed, 67 insertions(+), 41 deletions(-) diff --git a/ui/app/styles/components/dashboard-metric.scss b/ui/app/styles/components/dashboard-metric.scss index 3b6cfb8cb..f99ae030c 100644 --- a/ui/app/styles/components/dashboard-metric.scss +++ b/ui/app/styles/components/dashboard-metric.scss @@ -1,5 +1,7 @@ .dashboard-metric { - margin-top: 1.5em; + &:not(:last-child) { + margin-bottom: 1.5em; + } .metric { text-align: left; @@ -28,4 +30,17 @@ .annotation { margin-top: -0.75rem; } + + &.with-divider { + border-top: 1px solid $grey-blue; + padding-top: 1.5em; + } + + .pair { + font-size: $size-5; + } + + .is-faded { + color: darken($grey-blue, 20%); + } } diff --git a/ui/app/styles/core/columns.scss b/ui/app/styles/core/columns.scss index d912a37c3..f4cdd5e62 100644 --- a/ui/app/styles/core/columns.scss +++ b/ui/app/styles/core/columns.scss @@ -23,4 +23,8 @@ margin-left: auto; margin-right: auto; } + + &.is-flush { + margin-bottom: 0; + } } diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 7f641443b..a81ef106a 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -11,7 +11,7 @@ {{/each}} {{#if this.activeAllocation}} - + {{#each this.activeEdges as |edge|}} {{/each}} diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index d8dc5044b..af7f36eeb 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -27,47 +27,54 @@
{{if this.activeAllocation "Allocation" "Cluster"}} Details
{{#if this.activeAllocation}} -

- Allocation: - {{this.activeAllocation.shortId}} -

-

Sibling Allocations: {{this.siblingAllocations.length}}

-

Unique Client Placements: {{this.uniqueActiveAllocationNodes.length}}

-

-

- Job: - - {{this.activeAllocation.job.name}} - -

-

- Client: - - {{this.activeAllocation.node.name}} ({{this.activeAllocation.node.shortId}}) - -

- - - {{else}} -
-
-

DCs

-

{{this.datacenters.length}}

-
-
-

Clients

-

{{this.model.nodes.length}}

-
-
-

Allocations

- {{! TODO: make sure that this is only the scheduled allocations }} -

{{this.model.allocations.length}}

-
+
+

+ Allocation: + {{this.activeAllocation.shortId}} +

+

Sibling Allocations: {{this.siblingAllocations.length}}

+

Unique Client Placements: {{this.uniqueActiveAllocationNodes.length}}

+
+
+

+ Job: + + {{this.activeAllocation.job.name}} + / {{this.activeAllocation.taskGroupName}} +

+

Type: {{this.activeAllocation.job.type}}

+

Priority: {{this.activeAllocation.job.priority}}

+
+
+

+ Client: + + {{this.activeAllocation.node.shortId}} + +

+

Name: {{this.activeAllocation.node.name}}

+

Address: {{this.activeAllocation.node.httpAddr}}

+
+
+
+ +
+ {{else}} +
+
+

{{this.model.nodes.length}} Clients

+
+
+ {{! TODO: make sure that this is only the scheduled allocations }} +

{{this.model.allocations.length}} Allocations

+
+
+

{{this.totalMemoryFormatted}} {{this.totalMemoryUnits}} of memory

From 5b55f3c740f8bc9f4919e15a9077dd8448ca44b9 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Sep 2020 16:58:38 -0700 Subject: [PATCH 17/52] Medium scale topo scenario --- ui/mirage/scenarios/topo.js | 99 ++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/ui/mirage/scenarios/topo.js b/ui/mirage/scenarios/topo.js index e75bf7fa5..b2a2d2aec 100644 --- a/ui/mirage/scenarios/topo.js +++ b/ui/mirage/scenarios/topo.js @@ -42,12 +42,107 @@ export function topoSmall(server) { server.createList('allocation', 25, { forceRunningClientStatus: true, }); - //server.createList('allocation', 15, { clientStatus: 'pending' }); } export function topoSmallProblems(server) {} -export function topoMedium(server) {} +export function topoMedium(server) { + server.createList('agent', 3); + server.createList('node', 7, { + datacenter: 'us-west-1', + status: 'ready', + resources: { + CPU: 3000, + MemoryMB: 5192, + DiskMB: 10000, + IOPS: 100000, + Networks: generateNetworks(), + Ports: generatePorts(), + }, + }); + server.createList('node', 7, { + datacenter: 'us-east-1', + status: 'ready', + resources: { + CPU: 3000, + MemoryMB: 5192, + DiskMB: 10000, + IOPS: 100000, + Networks: generateNetworks(), + Ports: generatePorts(), + }, + }); + server.createList('node', 7, { + datacenter: 'eu-west-1', + status: 'ready', + resources: { + CPU: 3000, + MemoryMB: 5192, + DiskMB: 10000, + IOPS: 100000, + Networks: generateNetworks(), + Ports: generatePorts(), + }, + }); + + server.createList('node', 8, { + datacenter: 'us-west-1', + status: 'ready', + resources: { + CPU: 8000, + MemoryMB: 12192, + DiskMB: 10000, + IOPS: 100000, + Networks: generateNetworks(), + Ports: generatePorts(), + }, + }); + server.createList('node', 7, { + datacenter: 'us-east-1', + status: 'ready', + resources: { + CPU: 8000, + MemoryMB: 12192, + DiskMB: 10000, + IOPS: 100000, + Networks: generateNetworks(), + Ports: generatePorts(), + }, + }); + + const jobResources = [ + ['M: 2560, C: 150'], + ['M: 128, C: 400'], + ['M: 512, C: 100'], + ['M: 256, C: 150'], + ['M: 200, C: 50'], + ['M: 64, C: 100'], + ['M: 128, C: 150'], + ['M: 1024, C: 500'], + + ['M: 1200, C: 50'], + ['M: 1400, C: 200'], + ['M: 50, C: 150'], + ['M: 5000, C: 1800'], + + ['M: 100, C: 300', 'M: 200, C: 150'], + ['M: 512, C: 250', 'M: 600, C: 200'], + ]; + + jobResources.forEach(spec => { + server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'service', + createAllocations: false, + resourceSpec: spec, + }); + }); + + server.createList('allocation', 100, { + forceRunningClientStatus: true, + }); +} export function topoMediumBatch(server) {} From 4c155b5da413bf952392b084ac1007b0e8120646 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Sep 2020 16:58:50 -0700 Subject: [PATCH 18/52] CSS Grid based masonry layout TBH, it's buggy and I don't like it. --- ui/app/components/topo-viz.js | 26 +++++++++++++ ui/app/components/topo-viz/datacenter.js | 1 + ui/app/styles/charts.scss | 5 ++- ui/app/styles/charts/topo-viz.scss | 11 ++++++ ui/app/styles/core/typography.scss | 4 ++ ui/app/templates/components/topo-viz.hbs | 5 ++- .../components/topo-viz/datacenter.hbs | 38 ++++++++++--------- ui/app/templates/components/topo-viz/node.hbs | 6 ++- 8 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 ui/app/styles/charts/topo-viz.scss diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 59186b27f..af9415a5a 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -43,6 +43,32 @@ export default class TopoViz extends Component { .range([15, 30]) .domain([0, max(this.args.nodes.map(node => node.resources.memory))]); this.isLoaded = true; + + // schedule masonry + run.schedule('afterRender', () => { + this.masonry(); + }); + } + + @action + masonry() { + run.next(() => { + const datacenterSections = this.element.querySelectorAll('.topo-viz-datacenter'); + const elementStyles = window.getComputedStyle(this.element); + if (!elementStyles) return; + + const rowHeight = parseInt(elementStyles.getPropertyValue('grid-auto-rows')) || 0; + const rowGap = parseInt(elementStyles.getPropertyValue('grid-row-gap')) || 0; + + if (!rowHeight) return; + + for (let dc of datacenterSections) { + const contents = dc.querySelector('.masonry-container'); + const height = contents.getBoundingClientRect().height; + const rowSpan = Math.ceil((height + rowGap) / (rowHeight + rowGap)); + dc.style.gridRowEnd = `span ${rowSpan}`; + } + }); } @action diff --git a/ui/app/components/topo-viz/datacenter.js b/ui/app/components/topo-viz/datacenter.js index 0d224353c..5b0f8b507 100644 --- a/ui/app/components/topo-viz/datacenter.js +++ b/ui/app/components/topo-viz/datacenter.js @@ -42,5 +42,6 @@ export default class TopoVizNode extends Component { ); this.isLoaded = true; + this.args.onLoad && this.args.onLoad(); } } diff --git a/ui/app/styles/charts.scss b/ui/app/styles/charts.scss index cfa14ddbb..3d494e091 100644 --- a/ui/app/styles/charts.scss +++ b/ui/app/styles/charts.scss @@ -3,8 +3,9 @@ @import './charts/line-chart'; @import './charts/tooltip'; @import './charts/colors'; -@import './charts/chart-annotation.scss'; -@import './charts/topo-viz-node.scss'; +@import './charts/chart-annotation'; +@import './charts/topo-viz'; +@import './charts/topo-viz-node'; .inline-chart { height: 1.5rem; diff --git a/ui/app/styles/charts/topo-viz.scss b/ui/app/styles/charts/topo-viz.scss new file mode 100644 index 000000000..3a3c9b2d9 --- /dev/null +++ b/ui/app/styles/charts/topo-viz.scss @@ -0,0 +1,11 @@ +.topo-viz { + display: grid; + grid-template-columns: repeat(2, 1fr); + column-gap: 1.5em; + row-gap: 1.5em; + grid-auto-rows: 10px; + + &.is-single-column { + grid-template-columns: 100%; + } +} diff --git a/ui/app/styles/core/typography.scss b/ui/app/styles/core/typography.scss index 546320e67..9294c5f13 100644 --- a/ui/app/styles/core/typography.scss +++ b/ui/app/styles/core/typography.scss @@ -23,3 +23,7 @@ code { .is-interactive { cursor: pointer; } + +.is-faded { + color: darken($grey-blue, 20%); +} diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index a81ef106a..08ae144c6 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,4 +1,4 @@ -
+
{{#if this.isLoaded}} {{#each this.datacenters as |dc|}} + @activeJobId={{this.activeJobId}} + @onLoad={{action this.masonry}}/> {{/each}} {{#if this.activeAllocation}} diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index 90dab4312..bba4d90b0 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -1,21 +1,23 @@
-
- {{@datacenter}} - {{this.scheduledAllocations.length}} Allocs, - {{@nodes.length}} Nodes, - {{this.aggregatedAllocationResources.memory}}/{{this.aggregatedNodeResources.memory}} MiB, - {{this.aggregatedAllocationResources.cpu}}/{{this.aggregatedNodeResources.cpu}} Mhz -
-
- {{#if this.isLoaded}} - {{#each @nodes as |node|}} - - {{/each}} - {{/if}} +
+
+ {{@datacenter}} + {{this.scheduledAllocations.length}} Allocs + {{@nodes.length}} Nodes + {{this.aggregatedAllocationResources.memory}}/{{this.aggregatedNodeResources.memory}} MiB, + {{this.aggregatedAllocationResources.cpu}}/{{this.aggregatedNodeResources.cpu}} Mhz +
+
+ {{#if this.isLoaded}} + {{#each @nodes as |node|}} + + {{/each}} + {{/if}} +
diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 849849197..303f66977 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,5 +1,9 @@
-

{{@node.name}} ({{this.count}} allocs) ({{@node.resources.memory}} MiB, {{@node.resources.cpu}} Mhz) (% reserved would be nice)

+

+ {{@node.name}} + {{this.count}} Allocs + {{@node.resources.memory}} MiB, {{@node.resources.cpu}} Mhz +

From 872ca3dd5ccc08fbc4dbe25cc72a129fb6367496 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Sep 2020 17:18:11 -0700 Subject: [PATCH 19/52] Add more variety to the node heights --- ui/app/components/topo-viz.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index af9415a5a..08d0e7a60 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -3,7 +3,7 @@ import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { run } from '@ember/runloop'; import { scaleLinear } from 'd3-scale'; -import { max } from 'd3-array'; +import { extent } from 'd3-array'; import RSVP from 'rsvp'; export default class TopoViz extends Component { @@ -40,8 +40,8 @@ export default class TopoViz extends Component { // TODO: Make the range dynamic based on the extent of the domain this.heightScale = scaleLinear() - .range([15, 30]) - .domain([0, max(this.args.nodes.map(node => node.resources.memory))]); + .range([15, 40]) + .domain(extent(this.args.nodes.map(node => node.resources.memory))); this.isLoaded = true; // schedule masonry From 7d75421a7556862a12ff7c990018a95d63934096 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 17 Sep 2020 00:30:45 -0700 Subject: [PATCH 20/52] Make the alloc select render path more efficient by not needlessly recomputing data --- ui/app/components/topo-viz/node.js | 42 ++++++++++++++++++- ui/app/templates/components/topo-viz/node.hbs | 6 +-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/ui/app/components/topo-viz/node.js b/ui/app/components/topo-viz/node.js index c947cdbd4..c441697aa 100644 --- a/ui/app/components/topo-viz/node.js +++ b/ui/app/components/topo-viz/node.js @@ -1,6 +1,6 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; +import { action, set } from '@ember/object'; import { guidFor } from '@ember/object/internals'; export default class TopoVizNode extends Component { @@ -82,6 +82,18 @@ export default class TopoVizNode extends Component { this.data = this.computeData(this.dimensionsWidth); } + @action + updateRender(svg) { + // Only update all data when the width changes + const newWidth = svg.clientWidth - this.padding - this.paddingLeft; + if (newWidth !== this.dimensionsWidth) { + this.dimensionsWidth = newWidth; + this.data = this.computeData(this.dimensionsWidth); + } else { + this.data = this.setSelection(); + } + } + @action highlightAllocation(allocation) { this.activeAllocation = allocation; @@ -97,6 +109,34 @@ export default class TopoVizNode extends Component { if (this.args.onAllocationSelect) this.args.onAllocationSelect(allocation); } + containsActiveTaskGroup() { + return this.allocations.some( + allocation => + allocation.taskGroupName === this.args.activeTaskGroup && + allocation.belongsTo('job').id() === this.args.activeJobId + ); + } + + setSelection() { + this.data.cpu.forEach(cpu => { + set( + cpu, + 'isSelected', + cpu.allocation.taskGroupName === this.args.activeTaskGroup && + cpu.allocation.belongsTo('job').id() === this.args.activeJobId + ); + }); + this.data.memory.forEach(memory => { + set( + memory, + 'isSelected', + memory.allocation.taskGroupName === this.args.activeTaskGroup && + memory.allocation.belongsTo('job').id() === this.args.activeJobId + ); + }); + return this.data; + } + computeData(width) { // TODO: differentiate reserved and resources if (!this.args.node.resources) return; diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 303f66977..c9973e50f 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -4,7 +4,7 @@ {{this.count}} Allocs {{@node.resources.memory}} MiB, {{@node.resources.cpu}} Mhz

- + @@ -31,7 +31,7 @@ width="{{this.data.memoryRemainder.width}}px" height="{{this.height}}px" /> {{/if}} - {{#each this.data.memory as |memory|}} + {{#each this.data.memory key="allocation.id" as |memory|}} {{/if}} - {{#each this.data.cpu as |cpu|}} + {{#each this.data.cpu key="allocation.id" as |cpu|}} Date: Wed, 23 Sep 2020 18:10:11 -0700 Subject: [PATCH 21/52] Refactor topo viz to do as much computation upfront & use faster data structures Now all data loading happens in the TopoViz component as well as computation of resource proportions. Allocation selection state is also managed centrally uses a dedicated structure indexed by group key (job id and task group name). This way allocations don't need to be scanned at the node level, which is O(n) at the best (assuming no ember overhead on recomputes). --- ui/app/components/topo-viz.js | 117 ++++++++++++++++-- ui/app/components/topo-viz/datacenter.js | 21 +--- ui/app/components/topo-viz/node.js | 66 ++-------- ui/app/templates/components/topo-viz.hbs | 15 ++- .../components/topo-viz/datacenter.hbs | 20 ++- ui/app/templates/components/topo-viz/node.hbs | 30 ++--- 6 files changed, 157 insertions(+), 112 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 08d0e7a60..59476d03b 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -1,7 +1,8 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; +import { action, set } from '@ember/object'; import { run } from '@ember/runloop'; +import { task } from 'ember-concurrency'; import { scaleLinear } from 'd3-scale'; import { extent } from 'd3-array'; import RSVP from 'rsvp'; @@ -10,6 +11,7 @@ export default class TopoViz extends Component { @tracked heightScale = null; @tracked isLoaded = false; @tracked element = null; + @tracked topology = {}; @tracked activeAllocation = null; @tracked activeEdges = []; @@ -22,17 +24,88 @@ export default class TopoViz extends Component { return this.activeAllocation && this.activeAllocation.belongsTo('job').id(); } - get datacenters() { - const datacentersMap = this.args.nodes.reduce((datacenters, node) => { - if (!datacenters[node.datacenter]) datacenters[node.datacenter] = []; - datacenters[node.datacenter].push(node); + dataForNode(node) { + return { + node, + datacenter: node.datacenter, + memory: node.resources.memory, + cpu: node.resources.cpu, + allocations: [], + }; + } + + dataForAllocation(allocation, node) { + const jobId = allocation.belongsTo('job').id(); + return { + allocation, + node, + jobId, + groupKey: JSON.stringify([jobId, allocation.taskGroupName]), + memory: allocation.resources.memory, + cpu: allocation.resources.cpu, + memoryPercent: allocation.resources.memory / node.memory, + cpuPercent: allocation.resources.cpu / node.cpu, + isSelected: false, + }; + } + + @task(function*() { + const nodes = this.args.nodes; + const allocations = this.args.allocations; + + // Nodes are probably partials and we'll need the resources on them + // TODO: this is an API update waiting to happen. + yield RSVP.all(nodes.map(node => (node.isPartial ? node.reload() : RSVP.resolve(node)))); + + // Wrap nodes in a topo viz specific data structure and build an index to speed up allocation assignment + const nodeContainers = []; + const nodeIndex = {}; + nodes.forEach(node => { + const container = this.dataForNode(node); + nodeContainers.push(container); + nodeIndex[node.id] = container; + }); + + // Wrap allocations in a topo viz specific data structure, assign allocations to nodes, and build an allocation + // index keyed off of job and task group + const allocationIndex = {}; + allocations.forEach(allocation => { + const nodeId = allocation.belongsTo('node').id(); + const nodeContainer = nodeIndex[nodeId]; + if (!nodeContainer) + throw new Error(`Node ${nodeId} for alloc ${allocation.id} not in index???`); + + const allocationContainer = this.dataForAllocation(allocation, nodeContainer); + nodeContainer.allocations.push(allocationContainer); + + const key = allocationContainer.groupKey; + if (!allocationIndex[key]) allocationIndex[key] = []; + allocationIndex[key].push(allocationContainer); + }); + + // Group nodes into datacenters + const datacentersMap = nodeContainers.reduce((datacenters, nodeContainer) => { + if (!datacenters[nodeContainer.datacenter]) datacenters[nodeContainer.datacenter] = []; + datacenters[nodeContainer.datacenter].push(nodeContainer); return datacenters; }, {}); - return Object.keys(datacentersMap) + // Turn hash of datacenters into a sorted array + const datacenters = Object.keys(datacentersMap) .map(key => ({ name: key, nodes: datacentersMap[key] })) .sortBy('name'); - } + + const topology = { + datacenters, + allocationIndex, + selectedKey: null, + heightScale: scaleLinear() + .range([15, 40]) + .domain(extent(nodeContainers.mapBy('memory'))), + }; + this.topology = topology; + }) + buildTopology; @action async loadNodes() { @@ -81,11 +154,37 @@ export default class TopoViz extends Component { if (this.activeAllocation === allocation) { this.activeAllocation = null; this.activeEdges = []; + + if (this.topology.selectedKey) { + const selectedAllocations = this.topology.allocationIndex[this.topology.selectedKey]; + if (selectedAllocations) { + selectedAllocations.forEach(allocation => { + set(allocation, 'isSelected', false); + }); + } + set(this.topology, 'selectedKey', null); + } } else { this.activeAllocation = allocation; + const selectedAllocations = this.topology.allocationIndex[this.topology.selectedKey]; + if (selectedAllocations) { + selectedAllocations.forEach(allocation => { + set(allocation, 'isSelected', false); + }); + } + + set(this.topology, 'selectedKey', allocation.groupKey); + const newAllocations = this.topology.allocationIndex[this.topology.selectedKey]; + if (newAllocations) { + newAllocations.forEach(allocation => { + set(allocation, 'isSelected', true); + }); + } + this.computedActiveEdges(); } - if (this.args.onAllocationSelect) this.args.onAllocationSelect(this.activeAllocation); + if (this.args.onAllocationSelect) + this.args.onAllocationSelect(this.activeAllocation && this.activeAllocation.allocation); } @action @@ -93,7 +192,7 @@ export default class TopoViz extends Component { // Wait a render cycle run.next(() => { const activeEl = this.element.querySelector( - `[data-allocation-id="${this.activeAllocation.id}"]` + `[data-allocation-id="${this.activeAllocation.allocation.id}"]` ); const selectedAllocations = this.element.querySelectorAll('.memory .bar.is-selected'); const activeBBox = activeEl.getBoundingClientRect(); diff --git a/ui/app/components/topo-viz/datacenter.js b/ui/app/components/topo-viz/datacenter.js index 5b0f8b507..7ebe7f66b 100644 --- a/ui/app/components/topo-viz/datacenter.js +++ b/ui/app/components/topo-viz/datacenter.js @@ -1,19 +1,13 @@ -import RSVP from 'rsvp'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -export default class TopoVizNode extends Component { +export default class TopoVizDatacenter extends Component { @tracked scheduledAllocations = []; @tracked aggregatedNodeResources = { cpu: 0, memory: 0 }; - @tracked isLoaded = false; - - get aggregateNodeResources() { - return this.args.nodes.mapBy('resources'); - } get aggregatedAllocationResources() { - return this.scheduledAllocations.mapBy('resources').reduce( + return this.scheduledAllocations.reduce( (totals, allocation) => { totals.cpu += allocation.cpu; totals.memory += allocation.memory; @@ -24,15 +18,13 @@ export default class TopoVizNode extends Component { } @action - async loadAllocations() { - await RSVP.all(this.args.nodes.mapBy('allocations')); - - this.scheduledAllocations = this.args.nodes.reduce( - (all, node) => all.concat(node.allocations.filterBy('isScheduled')), + loadAllocations() { + this.scheduledAllocations = this.args.datacenter.nodes.reduce( + (all, node) => all.concat(node.allocations.filterBy('allocation.isScheduled')), [] ); - this.aggregatedNodeResources = this.args.nodes.mapBy('resources').reduce( + this.aggregatedNodeResources = this.args.datacenter.nodes.reduce( (totals, node) => { totals.cpu += node.cpu; totals.memory += node.memory; @@ -41,7 +33,6 @@ export default class TopoVizNode extends Component { { cpu: 0, memory: 0 } ); - this.isLoaded = true; this.args.onLoad && this.args.onLoad(); } } diff --git a/ui/app/components/topo-viz/node.js b/ui/app/components/topo-viz/node.js index c441697aa..48a260647 100644 --- a/ui/app/components/topo-viz/node.js +++ b/ui/app/components/topo-viz/node.js @@ -1,6 +1,6 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { action, set } from '@ember/object'; +import { action } from '@ember/object'; import { guidFor } from '@ember/object/internals'; export default class TopoVizNode extends Component { @@ -10,7 +10,7 @@ export default class TopoVizNode extends Component { @tracked activeAllocation = null; get height() { - return this.args.heightScale ? this.args.heightScale(this.args.node.resources.memory) : 15; + return this.args.heightScale ? this.args.heightScale(this.args.node.memory) : 15; } get labelHeight() { @@ -51,19 +51,15 @@ export default class TopoVizNode extends Component { } get count() { - return this.args.node.get('allocations.length'); + return this.args.node.allocations.length; } get allocations() { - // return this.args.node.allocations.filterBy('isScheduled').sortBy('resources.memory'); - const totalCPU = this.args.node.resources.cpu; - const totalMemory = this.args.node.resources.memory; - // Sort by the delta between memory and cpu percent. This creates the least amount of // drift between the positional alignment of an alloc's cpu and memory representations. - return this.args.node.allocations.filterBy('isScheduled').sort((a, b) => { - const deltaA = Math.abs(a.resources.memory / totalMemory - a.resources.cpu / totalCPU); - const deltaB = Math.abs(b.resources.memory / totalMemory - b.resources.cpu / totalCPU); + return this.args.node.allocations.filterBy('allocation.isScheduled').sort((a, b) => { + const deltaA = Math.abs(a.memoryPercent - a.cpuPercent); + const deltaB = Math.abs(b.memoryPercent - b.cpuPercent); return deltaA - deltaB; }); } @@ -89,8 +85,6 @@ export default class TopoVizNode extends Component { if (newWidth !== this.dimensionsWidth) { this.dimensionsWidth = newWidth; this.data = this.computeData(this.dimensionsWidth); - } else { - this.data = this.setSelection(); } } @@ -110,51 +104,23 @@ export default class TopoVizNode extends Component { } containsActiveTaskGroup() { - return this.allocations.some( + return this.args.node.allocations.some( allocation => allocation.taskGroupName === this.args.activeTaskGroup && allocation.belongsTo('job').id() === this.args.activeJobId ); } - setSelection() { - this.data.cpu.forEach(cpu => { - set( - cpu, - 'isSelected', - cpu.allocation.taskGroupName === this.args.activeTaskGroup && - cpu.allocation.belongsTo('job').id() === this.args.activeJobId - ); - }); - this.data.memory.forEach(memory => { - set( - memory, - 'isSelected', - memory.allocation.taskGroupName === this.args.activeTaskGroup && - memory.allocation.belongsTo('job').id() === this.args.activeJobId - ); - }); - return this.data; - } - computeData(width) { - // TODO: differentiate reserved and resources - if (!this.args.node.resources) return; - - const totalCPU = this.args.node.resources.cpu; - const totalMemory = this.args.node.resources.memory; + const allocations = this.allocations; let cpuOffset = 0; let memoryOffset = 0; const cpu = []; const memory = []; - for (const allocation of this.allocations) { - const cpuPercent = allocation.resources.cpu / totalCPU; - const memoryPercent = allocation.resources.memory / totalMemory; - const isFirst = allocation === this.allocations[0]; - const isSelected = - allocation.taskGroupName === this.args.activeTaskGroup && - allocation.belongsTo('job').id() === this.args.activeJobId; + for (const allocation of allocations) { + const { cpuPercent, memoryPercent, isSelected } = allocation; + const isFirst = allocation === allocations[0]; let cpuWidth = cpuPercent * width - 1; let memoryWidth = memoryPercent * width - 1; @@ -169,21 +135,19 @@ export default class TopoVizNode extends Component { cpu.push({ allocation, - isSelected, offset: cpuOffset * 100, percent: cpuPercent * 100, width: cpuWidth, x: cpuOffset * width + (isFirst ? 0 : 0.5) + (isSelected ? 0.5 : 0), - className: allocation.clientStatus, + className: allocation.allocation.clientStatus, }); memory.push({ allocation, - isSelected, offset: memoryOffset * 100, percent: memoryPercent * 100, width: memoryWidth, x: memoryOffset * width + (isFirst ? 0 : 0.5) + (isSelected ? 0.5 : 0), - className: allocation.clientStatus, + className: allocation.allocation.clientStatus, }); cpuOffset += cpuPercent; @@ -209,7 +173,3 @@ export default class TopoVizNode extends Component { }; } } - -// capture width on did insert element -// update width on window resize -// recompute data when width changes diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 08ae144c6..79f9029f8 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,13 +1,12 @@ -
- {{#if this.isLoaded}} - {{#each this.datacenters as |dc|}} +
+ {{#if this.buildTopology.isRunning}} +
+ {{else}} + {{#each this.topology.datacenters as |dc|}} {{/each}} diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index bba4d90b0..9366b9f0b 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -1,23 +1,19 @@
- {{@datacenter}} + {{@datacenter.name}} {{this.scheduledAllocations.length}} Allocs - {{@nodes.length}} Nodes + {{@datacenter.nodes.length}} Nodes {{this.aggregatedAllocationResources.memory}}/{{this.aggregatedNodeResources.memory}} MiB, {{this.aggregatedAllocationResources.cpu}}/{{this.aggregatedNodeResources.cpu}} Mhz
- {{#if this.isLoaded}} - {{#each @nodes as |node|}} - - {{/each}} - {{/if}} + {{#each @datacenter.nodes as |node|}} + + {{/each}}
diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index c9973e50f..0e60e9742 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,10 +1,10 @@

- {{@node.name}} + {{@node.node.name}} {{this.count}} Allocs - {{@node.resources.memory}} MiB, {{@node.resources.cpu}} Mhz + {{@node.memory}} MiB, {{@node.cpu}} Mhz

- + @@ -33,23 +33,23 @@ {{/if}} {{#each this.data.memory key="allocation.id" as |memory|}} {{#if (or (eq memory.className "starting") (eq memory.className "pending"))}} {{/if}} @@ -69,23 +69,23 @@ {{/if}} {{#each this.data.cpu key="allocation.id" as |cpu|}} {{#if (or (eq cpu.className "starting") (eq cpu.className "pending"))}} {{/if}} From 6d99f964251ed71e705b6b789cf26c449c66ccbc Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 23 Sep 2020 18:54:35 -0700 Subject: [PATCH 22/52] Connect the memory and cpu rectangles --- ui/app/components/topo-viz.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 59476d03b..f04cf617d 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -200,6 +200,7 @@ export default class TopoViz extends Component { const vLeft = window.visualViewport.pageLeft; const vTop = window.visualViewport.pageTop; + // Lines to the memory rect of each selected allocation const edges = []; for (let allocation of selectedAllocations) { if (allocation !== activeEl) { @@ -213,10 +214,23 @@ export default class TopoViz extends Component { } } + // Lines from the memory rect to the cpu rect + for (let allocation of selectedAllocations) { + const id = allocation.closest('[data-allocation-id]').dataset.allocationId; + const cpu = allocation + .closest('.topo-viz-node') + .querySelector(`.cpu .bar[data-allocation-id="${id}"]`); + const bboxMem = allocation.getBoundingClientRect(); + const bboxCpu = cpu.getBoundingClientRect(); + edges.push({ + x1: bboxMem.x + bboxMem.width / 2 + vLeft, + y1: bboxMem.y + bboxMem.height / 2 + vTop, + x2: bboxCpu.x + bboxCpu.width / 2 + vLeft, + y2: bboxCpu.y + bboxCpu.height / 2 + vTop, + }); + } + this.activeEdges = edges; }); - // get element for active alloc - // get element for all selected allocs - // draw lines between centroid of each } } From 99746a24acc0066916e6e063ac095eb06aea58b8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 23 Sep 2020 23:09:53 -0700 Subject: [PATCH 23/52] A better loading screen for the topo viz while nodes load --- ui/app/templates/components/topo-viz.hbs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 79f9029f8..78d3ca7ad 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,6 +1,10 @@ -
+
{{#if this.buildTopology.isRunning}} -
+
+

Loading. If you have a lot of clients this may take awhile

+

Every client needs to be loaded individually. This is a shortcoming of the prototype and will be fixed before this is graduated to the actual Nomad project.

+ +
{{else}} {{#each this.topology.datacenters as |dc|}} Date: Fri, 25 Sep 2020 16:37:51 -0700 Subject: [PATCH 24/52] Second attempt at a masonry layout --- ui/app/components/topo-viz.js | 69 ++++++++++++++----- ui/app/styles/charts/topo-viz.scss | 22 ++++-- ui/app/templates/components/topo-viz.hbs | 19 ++--- .../components/topo-viz/datacenter.hbs | 30 ++++---- ui/package.json | 2 +- ui/yarn.lock | 5 ++ 6 files changed, 98 insertions(+), 49 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index f04cf617d..dea22a8aa 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -4,24 +4,36 @@ import { action, set } from '@ember/object'; import { run } from '@ember/runloop'; import { task } from 'ember-concurrency'; import { scaleLinear } from 'd3-scale'; -import { extent } from 'd3-array'; +import { extent, deviation, mean, minIndex, max } from 'd3-array'; import RSVP from 'rsvp'; export default class TopoViz extends Component { @tracked heightScale = null; @tracked isLoaded = false; @tracked element = null; - @tracked topology = {}; + @tracked topology = { datacenters: [] }; @tracked activeAllocation = null; @tracked activeEdges = []; - get activeTaskGroup() { - return this.activeAllocation && this.activeAllocation.taskGroupName; + get isSingleColumn() { + if (this.topology.datacenters.length <= 1) return true; + + // Compute the coefficient of variance to determine if it would be + // better to stack datacenters or place them in columns + const nodeCounts = this.topology.datacenters.map(datacenter => datacenter.nodes.length); + const variationCoefficient = deviation(nodeCounts) / mean(nodeCounts); + + // The point at which the varation is too extreme for a two column layout + const threshold = 0.3; + if (variationCoefficient > threshold) return true; + return false; } - get activeJobId() { - return this.activeAllocation && this.activeAllocation.belongsTo('job').id(); + get datacenterIsSingleColumn() { + // If there are enough nodes, use two columns of nodes within + // a single column layout of datacenteres to increase density. + return this.columnsCount !== 1 || this.args.nodes.length <= 20; } dataForNode(node) { @@ -126,21 +138,44 @@ export default class TopoViz extends Component { @action masonry() { run.next(() => { + const columnCount = this.isSingleColumn ? 1 : 2; + + // There's nothing to do if this is single column layout + if (columnCount === 1) return; + + const columns = new Array(columnCount).fill(null).map(() => ({ + height: 0, + elements: [], + })); + const datacenterSections = this.element.querySelectorAll('.topo-viz-datacenter'); - const elementStyles = window.getComputedStyle(this.element); - if (!elementStyles) return; - - const rowHeight = parseInt(elementStyles.getPropertyValue('grid-auto-rows')) || 0; - const rowGap = parseInt(elementStyles.getPropertyValue('grid-row-gap')) || 0; - - if (!rowHeight) return; + // First pass: assign each element to a column based on the running heights of each column for (let dc of datacenterSections) { - const contents = dc.querySelector('.masonry-container'); - const height = contents.getBoundingClientRect().height; - const rowSpan = Math.ceil((height + rowGap) / (rowHeight + rowGap)); - dc.style.gridRowEnd = `span ${rowSpan}`; + const styles = window.getComputedStyle(dc); + const marginTop = parseFloat(styles.marginTop); + const marginBottom = parseFloat(styles.marginBottom); + const height = dc.clientHeight; + + // Pick the shortest column accounting for margins + const column = columns[minIndex(columns, c => c.height)]; + + // Add the new element's height to the column height + column.height += marginTop + height + marginBottom; + column.elements.push(dc); } + + // Second pass: assign an order to each element based on their column and position in the column + columns + .mapBy('elements') + .flat() + .forEach((dc, index) => { + dc.style.order = index; + }); + + // Set the max height of the container to the height of the tallest column + this.element.querySelector('.topo-viz-datacenters').style.maxHeight = + max(columns.mapBy('height')) + 1 + 'px'; }); } diff --git a/ui/app/styles/charts/topo-viz.scss b/ui/app/styles/charts/topo-viz.scss index 3a3c9b2d9..753d1af82 100644 --- a/ui/app/styles/charts/topo-viz.scss +++ b/ui/app/styles/charts/topo-viz.scss @@ -1,11 +1,19 @@ .topo-viz { - display: grid; - grid-template-columns: repeat(2, 1fr); - column-gap: 1.5em; - row-gap: 1.5em; - grid-auto-rows: 10px; + .topo-viz-datacenters { + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-content: space-between; + margin-top: -0.75em; - &.is-single-column { - grid-template-columns: 100%; + .topo-viz-datacenter { + margin-top: 0.75em; + margin-bottom: 0.75em; + width: calc(50% - 0.75em); + } + } + + &.is-single-column .topo-viz-datacenter { + width: 100%; } } diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 78d3ca7ad..dc62a1cad 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,4 +1,4 @@ -
+
{{#if this.buildTopology.isRunning}}

Loading. If you have a lot of clients this may take awhile

@@ -6,13 +6,16 @@
{{else}} - {{#each this.topology.datacenters as |dc|}} - - {{/each}} +
+ {{#each this.topology.datacenters as |dc|}} + + {{/each}} +
{{#if this.activeAllocation}} diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index 9366b9f0b..655976fca 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -1,19 +1,17 @@
-
-
- {{@datacenter.name}} - {{this.scheduledAllocations.length}} Allocs - {{@datacenter.nodes.length}} Nodes - {{this.aggregatedAllocationResources.memory}}/{{this.aggregatedNodeResources.memory}} MiB, - {{this.aggregatedAllocationResources.cpu}}/{{this.aggregatedNodeResources.cpu}} Mhz -
-
- {{#each @datacenter.nodes as |node|}} - - {{/each}} -
+
+ {{@datacenter.name}} + {{this.scheduledAllocations.length}} Allocs + {{@datacenter.nodes.length}} Nodes + {{this.aggregatedAllocationResources.memory}}/{{this.aggregatedNodeResources.memory}} MiB, + {{this.aggregatedAllocationResources.cpu}}/{{this.aggregatedNodeResources.cpu}} Mhz +
+
+ {{#each @datacenter.nodes as |node|}} + + {{/each}}
diff --git a/ui/package.json b/ui/package.json index 8ea295dea..cd1b4b292 100644 --- a/ui/package.json +++ b/ui/package.json @@ -43,7 +43,7 @@ "broccoli-asset-rev": "^3.0.0", "bulma": "0.6.1", "core-js": "^2.4.1", - "d3-array": "^1.2.0", + "d3-array": "^2.1.0", "d3-axis": "^1.0.0", "d3-format": "^1.3.0", "d3-scale": "^1.0.0", diff --git a/ui/yarn.lock b/ui/yarn.lock index 70a7edf24..4df1697a3 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -8312,6 +8312,11 @@ d3-array@^1.2.0: resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== +d3-array@^2.1.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.8.0.tgz#f76e10ad47f1f4f75f33db5fc322eb9ffde5ef23" + integrity sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw== + d3-axis@^1.0.0: version "1.0.12" resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9" From f27895c4c840d0947b12cd4130d971e29a39aac8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 Sep 2020 14:20:34 -0700 Subject: [PATCH 25/52] New FlexMasonry component implements a masonry layout using flexbox --- ui/app/components/flex-masonry.js | 66 ++++++++++++++++++++ ui/app/styles/components.scss | 1 + ui/app/styles/components/flex-masonry.scss | 37 +++++++++++ ui/app/templates/components/flex-masonry.hbs | 11 ++++ 4 files changed, 115 insertions(+) create mode 100644 ui/app/components/flex-masonry.js create mode 100644 ui/app/styles/components/flex-masonry.scss create mode 100644 ui/app/templates/components/flex-masonry.hbs diff --git a/ui/app/components/flex-masonry.js b/ui/app/components/flex-masonry.js new file mode 100644 index 000000000..afd9075b5 --- /dev/null +++ b/ui/app/components/flex-masonry.js @@ -0,0 +1,66 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { run } from '@ember/runloop'; +import { action } from '@ember/object'; +import { minIndex, max } from 'd3-array'; + +export default class FlexMasonry extends Component { + @tracked element = null; + + @action + captureElement(element) { + this.element = element; + } + + @action + reflow() { + run.next(() => { + // There's nothing to do if this is single column layout + if (!this.element || this.args.columns === 1 || !this.args.columns) return; + + const columns = new Array(this.args.columns).fill(null).map(() => ({ + height: 0, + elements: [], + })); + + const items = this.element.querySelectorAll('.flex-masonry-item'); + + // First pass: assign each element to a column based on the running heights of each column + for (let item of items) { + const styles = window.getComputedStyle(item); + const marginTop = parseFloat(styles.marginTop); + const marginBottom = parseFloat(styles.marginBottom); + const height = item.clientHeight; + + // Pick the shortest column accounting for margins + const column = columns[minIndex(columns, c => c.height)]; + + // Add the new element's height to the column height + column.height += marginTop + height + marginBottom; + column.elements.push(item); + } + + // Second pass: assign an order to each element based on their column and position in the column + columns + .mapBy('elements') + .flat() + .forEach((dc, index) => { + dc.style.order = index; + }); + + // Gaurantee column wrapping as predicted (if the first item of a column is shorter than the difference + // beteen the height of the column and the previous column, then flexbox will naturally place the first + // item at the end of the previous column). + columns.forEach((column, index) => { + const nextHeight = index < columns.length - 1 ? columns[index + 1].height : 0; + const item = column.elements.lastObject; + if (item) { + item.style.flexBasis = item.clientHeight + Math.max(0, nextHeight - column.height) + 'px'; + } + }); + + // Set the max height of the container to the height of the tallest column + this.element.style.maxHeight = max(columns.mapBy('height')) + 1 + 'px'; + }); + } +} diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index 40b4e1f7a..a032c11e6 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -12,6 +12,7 @@ @import './components/event'; @import './components/exec-button'; @import './components/exec-window'; +@import './components/flex-masonry'; @import './components/fs-explorer'; @import './components/global-search-container'; @import './components/global-search-dropdown'; diff --git a/ui/app/styles/components/flex-masonry.scss b/ui/app/styles/components/flex-masonry.scss new file mode 100644 index 000000000..743855672 --- /dev/null +++ b/ui/app/styles/components/flex-masonry.scss @@ -0,0 +1,37 @@ +.flex-masonry { + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-content: space-between; + margin-top: -0.75em; + + &.flex-masonry-columns-1 > .flex-masonry-item { + width: 100%; + } + &.flex-masonry-columns-2 > .flex-masonry-item { + width: 50%; + } + &.flex-masonry-columns-3 > .flex-masonry-item { + width: 33%; + } + &.flex-masonry-columns-4 > .flex-masonry-item { + width: 25%; + } + + &.with-spacing { + > .flex-masonry-item { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + + &.flex-masonry-columns-2 > .flex-masonry-item { + width: calc(50% - 0.75em); + } + &.flex-masonry-columns-3 > .flex-masonry-item { + width: calc(33% - 0.75em); + } + &.flex-masonry-columns-4 > .flex-masonry-item { + width: calc(25% - 0.75em); + } + } +} diff --git a/ui/app/templates/components/flex-masonry.hbs b/ui/app/templates/components/flex-masonry.hbs new file mode 100644 index 000000000..6d75f09f8 --- /dev/null +++ b/ui/app/templates/components/flex-masonry.hbs @@ -0,0 +1,11 @@ +
+ {{#each @items as |item|}} +
+ {{yield item (action this.reflow)}} +
+ {{/each}} +
From 1602d727893055b705d025890f7b478b0bc64c70 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 Sep 2020 14:22:00 -0700 Subject: [PATCH 26/52] Apply FlexMasonry to the TopoViz component --- ui/app/components/topo-viz.js | 55 ++---------------------- ui/app/templates/components/topo-viz.hbs | 10 ++--- 2 files changed, 7 insertions(+), 58 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index dea22a8aa..f317eca65 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -4,7 +4,7 @@ import { action, set } from '@ember/object'; import { run } from '@ember/runloop'; import { task } from 'ember-concurrency'; import { scaleLinear } from 'd3-scale'; -import { extent, deviation, mean, minIndex, max } from 'd3-array'; +import { extent, deviation, mean } from 'd3-array'; import RSVP from 'rsvp'; export default class TopoViz extends Component { @@ -25,7 +25,7 @@ export default class TopoViz extends Component { const variationCoefficient = deviation(nodeCounts) / mean(nodeCounts); // The point at which the varation is too extreme for a two column layout - const threshold = 0.3; + const threshold = 0.5; if (variationCoefficient > threshold) return true; return false; } @@ -33,7 +33,7 @@ export default class TopoViz extends Component { get datacenterIsSingleColumn() { // If there are enough nodes, use two columns of nodes within // a single column layout of datacenteres to increase density. - return this.columnsCount !== 1 || this.args.nodes.length <= 20; + return !this.isSingleColumn || (this.isSingleColumn && this.args.nodes.length <= 20); } dataForNode(node) { @@ -128,55 +128,6 @@ export default class TopoViz extends Component { .range([15, 40]) .domain(extent(this.args.nodes.map(node => node.resources.memory))); this.isLoaded = true; - - // schedule masonry - run.schedule('afterRender', () => { - this.masonry(); - }); - } - - @action - masonry() { - run.next(() => { - const columnCount = this.isSingleColumn ? 1 : 2; - - // There's nothing to do if this is single column layout - if (columnCount === 1) return; - - const columns = new Array(columnCount).fill(null).map(() => ({ - height: 0, - elements: [], - })); - - const datacenterSections = this.element.querySelectorAll('.topo-viz-datacenter'); - - // First pass: assign each element to a column based on the running heights of each column - for (let dc of datacenterSections) { - const styles = window.getComputedStyle(dc); - const marginTop = parseFloat(styles.marginTop); - const marginBottom = parseFloat(styles.marginBottom); - const height = dc.clientHeight; - - // Pick the shortest column accounting for margins - const column = columns[minIndex(columns, c => c.height)]; - - // Add the new element's height to the column height - column.height += marginTop + height + marginBottom; - column.elements.push(dc); - } - - // Second pass: assign an order to each element based on their column and position in the column - columns - .mapBy('elements') - .flat() - .forEach((dc, index) => { - dc.style.order = index; - }); - - // Set the max height of the container to the height of the tallest column - this.element.querySelector('.topo-viz-datacenters').style.maxHeight = - max(columns.mapBy('height')) + 1 + 'px'; - }); } @action diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index dc62a1cad..76374506e 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -6,16 +6,14 @@
{{else}} -
- {{#each this.topology.datacenters as |dc|}} - + - {{/each}} -
+ @onLoad={{action reflow}}/> + {{#if this.activeAllocation}} From 6e55d8a6ebd349550977d42f8a3322484c8eb4f3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 Sep 2020 14:23:58 -0700 Subject: [PATCH 27/52] Conditionally use the FlexMasonry layout for datacenters within TopoViz --- ui/app/styles/charts/topo-viz-node.scss | 4 ++++ ui/app/templates/components/topo-viz/datacenter.hbs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index e0006a614..61db0702b 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -62,6 +62,10 @@ } } +.flex-masonry-columns-2 > .flex-masonry-item > .chart.topo-viz-node svg { + width: calc(100% - 0.75em); +} + .chart.topo-viz-edges { width: 100%; height: 100%; diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index 655976fca..810f6e810 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -7,11 +7,11 @@ {{this.aggregatedAllocationResources.cpu}}/{{this.aggregatedNodeResources.cpu}} Mhz
- {{#each @datacenter.nodes as |node|}} + - {{/each}} +
From 066502d40870a51c48801cf24c5956ba0d1761b5 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 Sep 2020 16:46:56 -0700 Subject: [PATCH 28/52] Make the topo viz denser when there are >50 nodes By hiding node details and making nodes interactive instead, we can pack more allocations on a screen. --- ui/app/components/topo-viz.js | 27 ++++++++ ui/app/components/topo-viz/node.js | 7 ++ ui/app/controllers/topology.js | 30 ++++++++ ui/app/styles/charts/topo-viz-node.scss | 12 ++++ ui/app/templates/components/topo-viz.hbs | 2 + .../components/topo-viz/datacenter.hbs | 4 +- ui/app/templates/components/topo-viz/node.hbs | 14 ++-- ui/app/templates/topology.hbs | 68 ++++++++++++++++++- 8 files changed, 154 insertions(+), 10 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index f317eca65..a152ac6dc 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -13,6 +13,7 @@ export default class TopoViz extends Component { @tracked element = null; @tracked topology = { datacenters: [] }; + @tracked activeNode = null; @tracked activeAllocation = null; @tracked activeEdges = []; @@ -36,6 +37,12 @@ export default class TopoViz extends Component { return !this.isSingleColumn || (this.isSingleColumn && this.args.nodes.length <= 20); } + // Once a cluster is large enough, the exact details of a node are + // typically irrelevant and a waste of space. + get isDense() { + return this.args.nodes.length > 50; + } + dataForNode(node) { return { node, @@ -135,6 +142,21 @@ export default class TopoViz extends Component { this.element = element; } + @action + showNodeDetails(node) { + if (this.activeNode) { + set(this.activeNode, 'isSelected', false); + } + + this.activeNode = this.activeNode === node ? null : node; + + if (this.activeNode) { + set(this.activeNode, 'isSelected', true); + } + + if (this.args.onNodeSelect) this.args.onNodeSelect(this.activeNode); + } + @action associateAllocations(allocation) { if (this.activeAllocation === allocation) { @@ -151,6 +173,10 @@ export default class TopoViz extends Component { set(this.topology, 'selectedKey', null); } } else { + if (this.activeNode) { + set(this.activeNode, 'isSelected', false); + } + this.activeNode = null; this.activeAllocation = allocation; const selectedAllocations = this.topology.allocationIndex[this.topology.selectedKey]; if (selectedAllocations) { @@ -171,6 +197,7 @@ export default class TopoViz extends Component { } if (this.args.onAllocationSelect) this.args.onAllocationSelect(this.activeAllocation && this.activeAllocation.allocation); + if (this.args.onNodeSelect) this.args.onNodeSelect(this.activeNode); } @action diff --git a/ui/app/components/topo-viz/node.js b/ui/app/components/topo-viz/node.js index 48a260647..3bef48ee0 100644 --- a/ui/app/components/topo-viz/node.js +++ b/ui/app/components/topo-viz/node.js @@ -98,6 +98,13 @@ export default class TopoVizNode extends Component { this.activeAllocation = null; } + @action + selectNode() { + if (this.args.isDense && this.args.onNodeSelect) { + this.args.onNodeSelect(this.args.node.isSelected ? null : this.args.node); + } + } + @action selectAllocation(allocation) { if (this.args.onAllocationSelect) this.args.onAllocationSelect(allocation); diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index 200132e40..b676c01c6 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -68,6 +68,31 @@ export default class TopologyControllers extends Controller { }); } + @computed('activeNode') + get nodeUtilization() { + const node = this.activeNode; + const [formattedMemory, memoryUnits] = reduceToLargestUnit(node.memory * 1024 * 1024); + const totalReservedMemory = node.allocations + .mapBy('memory') + .reduce((sum, memory) => sum + (memory || 0), 0); + const totalReservedCPU = node.allocations + .mapBy('cpu') + .reduce((sum, cpu) => sum + (cpu || 0), 0); + + return { + totalMemoryFormatted: formattedMemory.toFixed(2), + totalMemoryUnits: memoryUnits, + + totalMemory: node.memory * 1024 * 1024, + totalReservedMemory: totalReservedMemory * 1024 * 1024, + reservedMemoryPercent: totalReservedMemory / node.memory, + + totalCPU: node.cpu, + totalReservedCPU, + reservedCPUPercent: totalReservedCPU / node.cpu, + }; + } + @computed('siblingAllocations.@each.node') get uniqueActiveAllocationNodes() { return this.siblingAllocations.mapBy('node').uniq(); @@ -80,4 +105,9 @@ export default class TopologyControllers extends Controller { allocation.reload(); } } + + @action + setNode(node) { + this.set('activeNode', node); + } } diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index 61db0702b..3f014450a 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -11,6 +11,17 @@ fill: $white-ter; stroke-width: 1; stroke: $grey-lighter; + + &.is-interactive:hover { + fill: $white; + stroke: $grey-light; + } + + &.is-selected, + &.is-selected:hover { + fill: $white; + stroke: $grey; + } } .dimension-background { @@ -42,6 +53,7 @@ alignment-baseline: central; font-weight: $weight-normal; fill: $grey; + pointer-events: none; } } diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 76374506e..6a604bf30 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -10,8 +10,10 @@ diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index 810f6e810..2798fbe91 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -10,8 +10,10 @@ + @onAllocationSelect={{@onAllocationSelect}} + @onNodeSelect={{@onNodeSelect}}/>
diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 0e60e9742..8a756fd03 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,16 +1,18 @@
-

- {{@node.node.name}} - {{this.count}} Allocs - {{@node.memory}} MiB, {{@node.cpu}} Mhz -

+ {{#unless @isDense}} +

+ {{@node.node.name}} + {{this.count}} Allocs + {{@node.memory}} MiB, {{@node.cpu}} Mhz +

+ {{/unless}} - + {{#if this.allocations.length}}
-
{{if this.activeAllocation "Allocation" "Cluster"}} Details
+
+ {{#if this.activeNode}}Client{{else if this.activeAllocation}}Allocation{{else}}Cluster{{/if}} Details +
- {{#if this.activeAllocation}} + {{#if this.activeNode}} +
+

{{this.activeNode.allocations.length}} Allocations

+
+
+

+ Client: + + {{this.activeNode.node.shortId}} + +

+

Name: {{this.activeNode.node.name}}

+

Address: {{this.activeNode.node.httpAddr}}

+
+
+

{{this.nodeUtilization.totalMemoryFormatted}} {{this.nodeUtilization.totalMemoryUnits}} of memory

+
+
+
+ + {{this.nodeUtilization.reservedMemoryPercent}} + +
+
+
+ {{format-percentage this.nodeUtilization.reservedMemoryPercent total=1}} +
+
+
+ {{format-bytes this.nodeUtilization.totalReservedMemory}} / {{format-bytes this.nodeUtilization.totalMemory}} reserved +
+
+
+

{{this.nodeUtilization.totalCPU}} Mhz of CPU

+
+
+
+ + {{this.nodeUtilization.reservedCPUPercent}} + +
+
+
+ {{format-percentage this.nodeUtilization.reservedCPUPercent total=1}} +
+
+
+ {{this.nodeUtilization.totalReservedCPU}} Mhz / {{this.nodeUtilization.totalCPU}} Mhz reserved +
+
+ {{else if this.activeAllocation}}

Allocation: @@ -121,7 +179,11 @@

- +
From ef12488d1cf1cbf0af08ecbcbf0b1d93ad78d658 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 7 Oct 2020 22:18:43 -0700 Subject: [PATCH 29/52] Fix a re-render bug with flexmasonry --- ui/app/components/flex-masonry.js | 2 +- ui/app/templates/components/flex-masonry.hbs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/app/components/flex-masonry.js b/ui/app/components/flex-masonry.js index afd9075b5..14760e6f7 100644 --- a/ui/app/components/flex-masonry.js +++ b/ui/app/components/flex-masonry.js @@ -48,7 +48,7 @@ export default class FlexMasonry extends Component { dc.style.order = index; }); - // Gaurantee column wrapping as predicted (if the first item of a column is shorter than the difference + // Guarantee column wrapping as predicted (if the first item of a column is shorter than the difference // beteen the height of the column and the previous column, then flexbox will naturally place the first // item at the end of the previous column). columns.forEach((column, index) => { diff --git a/ui/app/templates/components/flex-masonry.hbs b/ui/app/templates/components/flex-masonry.hbs index 6d75f09f8..8019f0dfc 100644 --- a/ui/app/templates/components/flex-masonry.hbs +++ b/ui/app/templates/components/flex-masonry.hbs @@ -2,7 +2,8 @@ class="flex-masonry {{if @withSpacing "with-spacing"}} flex-masonry-columns-{{@columns}}" {{did-insert this.captureElement}} {{did-insert this.reflow}} - {{did-update this.reflow}}> + {{did-update this.reflow}} + {{window-resize this.reflow}}> {{#each @items as |item|}}
{{yield item (action this.reflow)}} From f3aed88a10874126ebfc16d431a4084a6aeb0e2e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 7 Oct 2020 23:05:46 -0700 Subject: [PATCH 30/52] Improved curves for allocation associations --- ui/app/components/topo-viz.js | 115 ++++++++++++------ ui/app/styles/charts/topo-viz-node.scss | 15 --- ui/app/styles/charts/topo-viz.scss | 16 +++ .../styles/components/dashboard-metric.scss | 4 + ui/app/templates/components/topo-viz.hbs | 13 +- 5 files changed, 107 insertions(+), 56 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index a152ac6dc..8eab05e18 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -5,6 +5,7 @@ import { run } from '@ember/runloop'; import { task } from 'ember-concurrency'; import { scaleLinear } from 'd3-scale'; import { extent, deviation, mean } from 'd3-array'; +import { line, curveBasis } from 'd3-shape'; import RSVP from 'rsvp'; export default class TopoViz extends Component { @@ -16,6 +17,7 @@ export default class TopoViz extends Component { @tracked activeNode = null; @tracked activeAllocation = null; @tracked activeEdges = []; + @tracked edgeOffset = { x: 0, y: 0 }; get isSingleColumn() { if (this.topology.datacenters.length <= 1) return true; @@ -33,7 +35,7 @@ export default class TopoViz extends Component { get datacenterIsSingleColumn() { // If there are enough nodes, use two columns of nodes within - // a single column layout of datacenteres to increase density. + // a single column layout of datacenters to increase density. return !this.isSingleColumn || (this.isSingleColumn && this.args.nodes.length <= 20); } @@ -204,46 +206,85 @@ export default class TopoViz extends Component { computedActiveEdges() { // Wait a render cycle run.next(() => { - const activeEl = this.element.querySelector( - `[data-allocation-id="${this.activeAllocation.allocation.id}"]` - ); - const selectedAllocations = this.element.querySelectorAll('.memory .bar.is-selected'); - const activeBBox = activeEl.getBoundingClientRect(); + // const path = line().curve(curveCardinal.tension(0.5)); + const path = line().curve(curveBasis); + // 1. Get the active element + const allocation = this.activeAllocation.allocation; + const activeEl = this.element.querySelector(`[data-allocation-id="${allocation.id}"]`); + const activePoint = centerOfBBox(activeEl.getBoundingClientRect()); - const vLeft = window.visualViewport.pageLeft; - const vTop = window.visualViewport.pageTop; - - // Lines to the memory rect of each selected allocation - const edges = []; - for (let allocation of selectedAllocations) { - if (allocation !== activeEl) { - const bbox = allocation.getBoundingClientRect(); - edges.push({ - x1: activeBBox.x + activeBBox.width / 2 + vLeft, - y1: activeBBox.y + activeBBox.height / 2 + vTop, - x2: bbox.x + bbox.width / 2 + vLeft, - y2: bbox.y + bbox.height / 2 + vTop, - }); - } - } - - // Lines from the memory rect to the cpu rect - for (let allocation of selectedAllocations) { - const id = allocation.closest('[data-allocation-id]').dataset.allocationId; - const cpu = allocation + // 2. Collect the mem and cpu pairs for all selected allocs + const selectedMem = Array.from(this.element.querySelectorAll('.memory .bar.is-selected')); + const selectedPairs = selectedMem.map(mem => { + const id = mem.closest('[data-allocation-id]').dataset.allocationId; + const cpu = mem .closest('.topo-viz-node') .querySelector(`.cpu .bar[data-allocation-id="${id}"]`); - const bboxMem = allocation.getBoundingClientRect(); - const bboxCpu = cpu.getBoundingClientRect(); - edges.push({ - x1: bboxMem.x + bboxMem.width / 2 + vLeft, - y1: bboxMem.y + bboxMem.height / 2 + vTop, - x2: bboxCpu.x + bboxCpu.width / 2 + vLeft, - y2: bboxCpu.y + bboxCpu.height / 2 + vTop, - }); - } + return [mem, cpu]; + }); + const selectedPoints = selectedPairs.map(pair => { + return pair.map(el => centerOfBBox(el.getBoundingClientRect())); + }); - this.activeEdges = edges; + // 3. For each pair, compute the midpoint of the truncated triangle of points [Mem, Cpu, Active] + selectedPoints.forEach(points => { + const d1 = pointBetween(points[0], activePoint, 100, 0.5); + const d2 = pointBetween(points[1], activePoint, 100, 0.5); + points.push(midpoint(d1, d2)); + }); + + // 4. Generate curves for each active->mem and active->cpu pair going through the bisector + const curves = []; + // Steps are used to restrict the range of curves. The closer control points are placed, the less + // curvature the curve generator will generate. + const stepsMain = [0, 0.8, 1.0]; + // The second prong the fork does not need to retrace the entire path from the activePoint + const stepsSecondary = [0.8, 1.0]; + selectedPoints.forEach(points => { + curves.push( + curveFromPoints(...pointsAlongPath(activePoint, points[2], stepsMain), points[0]), + curveFromPoints(...pointsAlongPath(activePoint, points[2], stepsSecondary), points[1]) + ); + }); + + this.activeEdges = curves.map(curve => path(curve)); + this.edgeOffset = { x: window.visualViewport.pageLeft, y: window.visualViewport.pageTop }; }); } } + +function centerOfBBox(bbox) { + return { + x: bbox.x + bbox.width / 2, + y: bbox.y + bbox.height / 2, + }; +} + +function dist(p1, p2) { + return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); +} + +// Return the point between p1 and p2 at len (or pct if len > dist(p1, p2)) +function pointBetween(p1, p2, len, pct) { + const d = dist(p1, p2); + const ratio = d < len ? pct : len / d; + return pointBetweenPct(p1, p2, ratio); +} + +function pointBetweenPct(p1, p2, pct) { + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + return { x: p1.x + dx * pct, y: p1.y + dy * pct }; +} + +function pointsAlongPath(p1, p2, pcts) { + return pcts.map(pct => pointBetweenPct(p1, p2, pct)); +} + +function midpoint(p1, p2) { + return pointBetweenPct(p1, p2, 0.5); +} + +function curveFromPoints(...points) { + return points.map(p => [p.x, p.y]); +} diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index 3f014450a..0baadc7c0 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -77,18 +77,3 @@ .flex-masonry-columns-2 > .flex-masonry-item > .chart.topo-viz-node svg { width: calc(100% - 0.75em); } - -.chart.topo-viz-edges { - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - pointer-events: none; - overflow: visible; - - .edge { - stroke-width: 2; - stroke: $blue; - } -} diff --git a/ui/app/styles/charts/topo-viz.scss b/ui/app/styles/charts/topo-viz.scss index 753d1af82..bfbccb29d 100644 --- a/ui/app/styles/charts/topo-viz.scss +++ b/ui/app/styles/charts/topo-viz.scss @@ -16,4 +16,20 @@ &.is-single-column .topo-viz-datacenter { width: 100%; } + + .topo-viz-edges { + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + pointer-events: none; + overflow: visible; + + .edge { + stroke-width: 2; + stroke: $blue; + fill: none; + } + } } diff --git a/ui/app/styles/components/dashboard-metric.scss b/ui/app/styles/components/dashboard-metric.scss index f99ae030c..5b04440a1 100644 --- a/ui/app/styles/components/dashboard-metric.scss +++ b/ui/app/styles/components/dashboard-metric.scss @@ -3,6 +3,10 @@ margin-bottom: 1.5em; } + &.column:not(:last-child) { + margin-bottom: 0; + } + .metric { text-align: left; font-weight: $weight-bold; diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 6a604bf30..8648ab396 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -6,7 +6,10 @@
{{else}} - + - {{#each this.activeEdges as |edge|}} - - {{/each}} + + {{#each this.activeEdges as |edge|}} + + {{/each}} + {{/if}} {{/if}} From 7477f320120aea589de542214cf338174c76e723 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 10 Oct 2020 12:20:41 -0700 Subject: [PATCH 31/52] Label empty clients in the topo viz chart --- ui/app/styles/charts/topo-viz-node.scss | 16 ++++++++++++++-- ui/app/templates/components/topo-viz/node.hbs | 8 ++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index 0baadc7c0..ac2f9ea86 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -1,7 +1,11 @@ .chart.topo-viz-node { display: block; - svg { + .label { + font-weight: $weight-normal; + } + + .chart { display: inline-block; height: 100%; width: 100%; @@ -57,6 +61,13 @@ } } + .empty-text { + fill: $red; + transform: translate(50%, 50%); + text-anchor: middle; + alignment-baseline: central; + } + & + .topo-viz-node { margin-top: 1em; } @@ -74,6 +85,7 @@ } } -.flex-masonry-columns-2 > .flex-masonry-item > .chart.topo-viz-node svg { +.flex-masonry-columns-2 > .flex-masonry-item > .chart.topo-viz-node svg, +.flex-masonry-columns-2 > .flex-masonry-item > .chart.topo-viz-node .label { width: calc(100% - 0.75em); } diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 8a756fd03..73c88d5b0 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,7 +1,9 @@
{{#unless @isDense}} -

- {{@node.node.name}} +

+ + {{@node.node.name}} + {{this.count}} Allocs {{@node.memory}} MiB, {{@node.cpu}} Mhz

@@ -94,6 +96,8 @@ {{/each}} + {{else}} + Empty Client {{/if}}
From d9ac6a63c6e6b3702c81102a2786ccd46d217d75 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 10 Oct 2020 12:33:48 -0700 Subject: [PATCH 32/52] Add icons to denote draining or ineligibility of clients --- ui/app/styles/charts/topo-viz-node.scss | 6 +++--- ui/app/templates/components/topo-viz/node.hbs | 15 +++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index ac2f9ea86..cfef33320 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -1,4 +1,4 @@ -.chart.topo-viz-node { +.topo-viz-node { display: block; .label { @@ -85,7 +85,7 @@ } } -.flex-masonry-columns-2 > .flex-masonry-item > .chart.topo-viz-node svg, -.flex-masonry-columns-2 > .flex-masonry-item > .chart.topo-viz-node .label { +.flex-masonry-columns-2 > .flex-masonry-item > .topo-viz-node .chart, +.flex-masonry-columns-2 > .flex-masonry-item > .topo-viz-node .label { width: calc(100% - 0.75em); } diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 73c88d5b0..8f09f1d6d 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,9 +1,12 @@ -
+
{{#unless @isDense}}

- - {{@node.node.name}} - + {{#if @node.node.isDraining}} + {{x-icon "clock-outline" class="is-info"}} + {{else if (not @node.node.isEligible)}} + {{x-icon "lock-closed" class="is-warning"}} + {{/if}} + {{@node.node.name}} {{this.count}} Allocs {{@node.memory}} MiB, {{@node.cpu}} Mhz

@@ -100,9 +103,5 @@ Empty Client {{/if}} -
- {{this.activeAllocation.name}} {{this.activeAllocation.shortId}} - ({{this.activeAllocation.resources.memory}} MiB, {{this.activeAllocation.resources.cpu}} Mhz) -
From ba8675ae87361b85714bae6f967118d87a21852e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 10 Oct 2020 15:37:18 -0700 Subject: [PATCH 33/52] Filter total alloc count by only scheduled allocs --- ui/app/controllers/topology.js | 6 ++++++ ui/app/templates/topology.hbs | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index b676c01c6..f2056ee4d 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -5,10 +5,16 @@ import { reduceToLargestUnit } from 'nomad-ui/helpers/format-bytes'; @classic export default class TopologyControllers extends Controller { + @computed('model.nodes.@each.datacenter') get datacenters() { return Array.from(new Set(this.model.nodes.mapBy('datacenter'))).compact(); } + @computed('model.allocations.@each.isScheduled') + get scheduledAllocations() { + return this.model.allocations.filterBy('isScheduled'); + } + @computed('model.nodes.@each.resources') get totalMemory() { const mibs = this.model.nodes diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index f94f20e23..e7e618f3a 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -128,8 +128,7 @@

{{this.model.nodes.length}} Clients

- {{! TODO: make sure that this is only the scheduled allocations }} -

{{this.model.allocations.length}} Allocations

+

{{this.scheduledAllocations.length}} Allocations

From e21a2a03b23d0426e3e1d6fe531b9b4b2cfa8c2c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 10 Oct 2020 17:10:12 -0700 Subject: [PATCH 34/52] More information about clients in the info panel --- ui/app/templates/topology.hbs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index e7e618f3a..4d562e0ed 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -29,21 +29,35 @@
{{#if this.activeNode}} + {{#let this.activeNode.node as |node|}}

{{this.activeNode.allocations.length}} Allocations

Client: - - {{this.activeNode.node.shortId}} + + {{node.shortId}}

-

Name: {{this.activeNode.node.name}}

-

Address: {{this.activeNode.node.httpAddr}}

+

Name: {{node.name}}

+

Address: {{node.httpAddr}}

+

Status: {{node.status}}

+
+
+

+ Draining? {{if node.isDraining "Yes" "No"}} +

+

+ Eligible? {{if node.isEligible "Yes" "No"}} +

-

{{this.nodeUtilization.totalMemoryFormatted}} {{this.nodeUtilization.totalMemoryUnits}} of memory

+

+ {{this.nodeUtilization.totalMemoryFormatted}} + {{this.nodeUtilization.totalMemoryUnits}} + of memory +

@@ -84,6 +98,7 @@ {{this.nodeUtilization.totalReservedCPU}} Mhz / {{this.nodeUtilization.totalCPU}} Mhz reserved
+ {{/let}} {{else if this.activeAllocation}}

From 78b1efc53116979260f16bef708874efbec25723 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 10 Oct 2020 20:39:07 -0700 Subject: [PATCH 35/52] FlexMasonry integration tests --- ui/app/templates/components/flex-masonry.hbs | 3 +- .../components/flex-masonry-test.js | 168 ++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 ui/tests/integration/components/flex-masonry-test.js diff --git a/ui/app/templates/components/flex-masonry.hbs b/ui/app/templates/components/flex-masonry.hbs index 8019f0dfc..81777bf7b 100644 --- a/ui/app/templates/components/flex-masonry.hbs +++ b/ui/app/templates/components/flex-masonry.hbs @@ -1,11 +1,12 @@
{{#each @items as |item|}} -
+
{{yield item (action this.reflow)}}
{{/each}} diff --git a/ui/tests/integration/components/flex-masonry-test.js b/ui/tests/integration/components/flex-masonry-test.js new file mode 100644 index 000000000..62bded80f --- /dev/null +++ b/ui/tests/integration/components/flex-masonry-test.js @@ -0,0 +1,168 @@ +import { htmlSafe } from '@ember/template'; +import { click, find, findAll, settled } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; + +// Used to prevent XSS warnings in console +const h = height => htmlSafe(`height:${height}px`); + +module('Integration | Component | FlexMasonry', function(hooks) { + setupRenderingTest(hooks); + + test('presents as a single div when @items is empty', async function(assert) { + this.setProperties({ + items: [], + }); + + await this.render(hbs` + + + `); + + const div = find('[data-test-flex-masonry]'); + assert.ok(div); + assert.equal(div.tagName.toLowerCase(), 'div'); + assert.equal(div.children.length, 0); + + await componentA11yAudit(this.element, assert); + }); + + test('each item in @items gets wrapped in a flex-masonry-item wrapper', async function(assert) { + this.setProperties({ + items: ['one', 'two', 'three'], + columns: 2, + }); + + await this.render(hbs` + +

{{item}}

+
+ `); + + assert.equal(findAll('[data-test-flex-masonry-item]').length, this.items.length); + }); + + test('the @withSpacing arg adds the with-spacing class', async function(assert) { + await this.render(hbs` + + + `); + + assert.ok(find('[data-test-flex-masonry]').classList.contains('with-spacing')); + }); + + test('individual items along with the reflow action are yielded', async function(assert) { + this.setProperties({ + items: ['one', 'two'], + columns: 2, + height: h(50), + }); + + await this.render(hbs` + +
{{item}}
+
+ `); + + const div = find('[data-test-flex-masonry]'); + assert.equal(div.style.maxHeight, '51px'); + assert.ok(div.textContent.includes('one')); + assert.ok(div.textContent.includes('two')); + + this.set('height', h(500)); + await settled(); + assert.equal(div.style.maxHeight, '51px'); + + // The height of the div changes when reflow is called + await click('[data-test-flex-masonry-item]:first-child div'); + await settled(); + assert.equal(div.style.maxHeight, '501px'); + }); + + test('items are rendered to the DOM in the order they were passed into the component', async function(assert) { + this.setProperties({ + items: [ + { text: 'One', height: h(20) }, + { text: 'Two', height: h(100) }, + { text: 'Three', height: h(20) }, + { text: 'Four', height: h(20) }, + ], + columns: 2, + }); + + await this.render(hbs` + +
{{item.text}}
+
+ `); + + findAll('[data-test-flex-masonry-item]').forEach((el, index) => { + assert.equal(el.textContent.trim(), this.items[index].text); + }); + }); + + test('each item gets an order property', async function(assert) { + this.setProperties({ + items: [ + { text: 'One', height: h(20), expectedOrder: 0 }, + { text: 'Two', height: h(100), expectedOrder: 3 }, + { text: 'Three', height: h(20), expectedOrder: 1 }, + { text: 'Four', height: h(20), expectedOrder: 2 }, + ], + columns: 2, + }); + + await this.render(hbs` + +
{{item.text}}
+
+ `); + + findAll('[data-test-flex-masonry-item]').forEach((el, index) => { + assert.equal(el.style.order, this.items[index].expectedOrder); + }); + }); + + test('the last item in each column gets a specific flex-basis value', async function(assert) { + this.setProperties({ + items: [ + { text: 'One', height: h(20) }, + { text: 'Two', height: h(100), flexBasis: '100px' }, + { text: 'Three', height: h(20) }, + { text: 'Four', height: h(100), flexBasis: '100px' }, + { text: 'Five', height: h(20), flexBasis: '80px' }, + { text: 'Six', height: h(20), flexBasis: '80px' }, + ], + columns: 4, + }); + + await this.render(hbs` + +
{{item.text}}
+
+ `); + + findAll('[data-test-flex-masonry-item]').forEach((el, index) => { + if (el.style.flexBasis) { + assert.equal(el.style.flexBasis, this.items[index].flexBasis); + } + }); + }); +}); From b2b7d5e19e257b7e61ff9d4ffdb7c628899ded1f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sun, 11 Oct 2020 22:58:44 -0700 Subject: [PATCH 36/52] Test coverage for TopoViz::Node --- ui/app/components/topo-viz.js | 1 + ui/app/templates/components/topo-viz/node.hbs | 21 +- .../components/topo-viz/node-test.js | 339 ++++++++++++++++++ ui/tests/pages/components/topo-viz/node.js | 35 ++ 4 files changed, 390 insertions(+), 6 deletions(-) create mode 100644 ui/tests/integration/components/topo-viz/node-test.js create mode 100644 ui/tests/pages/components/topo-viz/node.js diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 8eab05e18..b01fe0315 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -52,6 +52,7 @@ export default class TopoViz extends Component { memory: node.resources.memory, cpu: node.resources.cpu, allocations: [], + isSelected: false, }; } diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 8f09f1d6d..0e4eb98cd 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,10 +1,10 @@ -
+
{{#unless @isDense}} -

+

{{#if @node.node.isDraining}} - {{x-icon "clock-outline" class="is-info"}} + {{x-icon "clock-outline" class="is-info"}} {{else if (not @node.node.isEligible)}} - {{x-icon "lock-closed" class="is-warning"}} + {{x-icon "lock-closed" class="is-warning"}} {{/if}} {{@node.node.name}} {{this.count}} Allocs @@ -17,7 +17,14 @@ - + {{#if this.allocations.length}} {{else}} - Empty Client + Empty Client {{/if}}

diff --git a/ui/tests/integration/components/topo-viz/node-test.js b/ui/tests/integration/components/topo-viz/node-test.js new file mode 100644 index 000000000..14f1f903a --- /dev/null +++ b/ui/tests/integration/components/topo-viz/node-test.js @@ -0,0 +1,339 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; +import { create } from 'ember-cli-page-object'; +import sinon from 'sinon'; +import faker from 'nomad-ui/mirage/faker'; +import topoVisNodePageObject from 'nomad-ui/tests/pages/components/topo-viz/node'; + +const TopoVizNode = create(topoVisNodePageObject()); + +const nodeGen = (name, datacenter, memory, cpu, flags = {}) => ({ + datacenter, + memory, + cpu, + isSelected: !!flags.isSelected, + node: { + name, + isEligible: flags.isEligible || flags.isEligible == null, + isDraining: !!flags.isDraining, + }, +}); + +const allocGen = (node, memory, cpu, isSelected) => ({ + memory, + cpu, + isSelected, + memoryPercent: memory / node.memory, + cpuPercent: cpu / node.cpu, + allocation: { + id: faker.random.uuid(), + isScheduled: true, + }, +}); + +const props = overrides => ({ + isDense: false, + heightScale: () => 50, + onAllocationSelect: sinon.spy(), + onNodeSelect: sinon.spy(), + ...overrides, +}); + +module('Integration | Component | TopoViz::Node', function(hooks) { + setupRenderingTest(hooks); + + const commonTemplate = hbs` + + `; + + test('presents as a div with a label and an svg with CPU and memory rows', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + this.setProperties( + props({ + node: { + ...node, + allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(TopoVizNode.isPresent); + assert.ok(TopoVizNode.memoryRects.length); + assert.ok(TopoVizNode.cpuRects.length); + + await componentA11yAudit(this.element, assert); + }); + + test('the label contains aggregate information about the node', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + this.setProperties( + props({ + node: { + ...node, + allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(TopoVizNode.label.includes(node.node.name)); + assert.ok(TopoVizNode.label.includes(`${this.node.allocations.length} Allocs`)); + assert.ok(TopoVizNode.label.includes(`${this.node.memory} MiB`)); + assert.ok(TopoVizNode.label.includes(`${this.node.cpu} Mhz`)); + }); + + test('the status icon indicates when the node is draining', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000, { isDraining: true }); + this.setProperties( + props({ + node: { + ...node, + allocations: [], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(TopoVizNode.statusIcon.includes('icon-is-clock-outline')); + assert.equal(TopoVizNode.statusIconLabel, 'Client is draining'); + }); + + test('the status icon indicates when the node is ineligible for scheduling', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000, { isEligible: false }); + this.setProperties( + props({ + node: { + ...node, + allocations: [], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(TopoVizNode.statusIcon.includes('icon-is-lock-closed')); + assert.equal(TopoVizNode.statusIconLabel, 'Client is ineligible'); + }); + + test('when isDense is false, clicking the node does nothing', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + this.setProperties( + props({ + isDense: false, + node: { + ...node, + allocations: [], + }, + }) + ); + + await this.render(commonTemplate); + await TopoVizNode.selectNode(); + + assert.notOk(TopoVizNode.nodeIsInteractive); + assert.notOk(this.onNodeSelect.called); + }); + + test('when isDense is true, clicking the node calls onNodeSelect', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + this.setProperties( + props({ + isDense: true, + node: { + ...node, + allocations: [], + }, + }) + ); + + await this.render(commonTemplate); + await TopoVizNode.selectNode(); + + assert.ok(TopoVizNode.nodeIsInteractive); + assert.ok(this.onNodeSelect.called); + assert.ok(this.onNodeSelect.calledWith(this.node)); + }); + + test('the node gets the is-selected class when the node is selected', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000, { isSelected: true }); + this.setProperties( + props({ + isDense: true, + node: { + ...node, + allocations: [], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(TopoVizNode.nodeIsSelected); + }); + + test('the node gets its height form the @heightScale arg', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + const height = 50; + const heightSpy = sinon.spy(); + this.setProperties( + props({ + heightScale: (...args) => { + heightSpy(...args); + return height; + }, + node: { + ...node, + allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(heightSpy.called); + assert.ok(heightSpy.calledWith(this.node.memory)); + assert.equal(TopoVizNode.memoryRects[0].height, `${height}px`); + }); + + test('each allocation gets a memory rect and a cpu rect', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + this.setProperties( + props({ + node: { + ...node, + allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], + }, + }) + ); + + await this.render(commonTemplate); + + assert.equal(TopoVizNode.memoryRects.length, this.node.allocations.length); + assert.equal(TopoVizNode.cpuRects.length, this.node.allocations.length); + }); + + test('each allocation is sized according to its percentage of utilization', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + this.setProperties( + props({ + node: { + ...node, + allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], + }, + }) + ); + + await this.render(hbs` +
+ +
+ `); + + // Remove the width of the padding and the label from the SVG width + const width = 100 - 5 - 5 - 20; + this.node.allocations.forEach((alloc, index) => { + const memWidth = alloc.memoryPercent * width - (index === 0 ? 0.5 : 1); + const cpuWidth = alloc.cpuPercent * width - (index === 0 ? 0.5 : 1); + assert.equal(TopoVizNode.memoryRects[index].width, `${memWidth}px`); + assert.equal(TopoVizNode.cpuRects[index].width, `${cpuWidth}px`); + }); + }); + + test('clicking either the memory or cpu rect for an allocation will call onAllocationSelect', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + this.setProperties( + props({ + node: { + ...node, + allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], + }, + }) + ); + + await this.render(commonTemplate); + + await TopoVizNode.memoryRects[0].select(); + assert.ok(this.onAllocationSelect.callCount, 1); + assert.ok(this.onAllocationSelect.calledWith(this.node.allocations[0])); + + await TopoVizNode.cpuRects[0].select(); + assert.ok(this.onAllocationSelect.callCount, 2); + + await TopoVizNode.cpuRects[1].select(); + assert.ok(this.onAllocationSelect.callCount, 3); + assert.ok(this.onAllocationSelect.calledWith(this.node.allocations[1])); + + await TopoVizNode.memoryRects[1].select(); + assert.ok(this.onAllocationSelect.callCount, 4); + }); + + test('allocations are sorted by smallest to largest delta of memory to cpu percent utilizations', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + + const evenAlloc = allocGen(node, 100, 100); + const mediumMemoryAlloc = allocGen(node, 200, 150); + const largeMemoryAlloc = allocGen(node, 300, 50); + const mediumCPUAlloc = allocGen(node, 150, 200); + const largeCPUAlloc = allocGen(node, 50, 300); + + this.setProperties( + props({ + node: { + ...node, + allocations: [ + largeCPUAlloc, + mediumCPUAlloc, + evenAlloc, + mediumMemoryAlloc, + largeMemoryAlloc, + ], + }, + }) + ); + + await this.render(commonTemplate); + + const expectedOrder = [ + evenAlloc, + mediumCPUAlloc, + mediumMemoryAlloc, + largeCPUAlloc, + largeMemoryAlloc, + ]; + expectedOrder.forEach((alloc, index) => { + assert.equal(TopoVizNode.memoryRects[index].id, alloc.allocation.id); + assert.equal(TopoVizNode.cpuRects[index].id, alloc.allocation.id); + }); + }); + + test('when there are no allocations, a "no allocations" note is shown', async function(assert) { + const node = nodeGen('Node One', 'dc1', 1000, 1000); + this.setProperties( + props({ + node: { + ...node, + allocations: [], + }, + }) + ); + + await this.render(commonTemplate); + assert.equal(TopoVizNode.emptyMessage, 'Empty Client'); + }); +}); diff --git a/ui/tests/pages/components/topo-viz/node.js b/ui/tests/pages/components/topo-viz/node.js new file mode 100644 index 000000000..5dac302a7 --- /dev/null +++ b/ui/tests/pages/components/topo-viz/node.js @@ -0,0 +1,35 @@ +import { attribute, collection, clickable, hasClass, text } from 'ember-cli-page-object'; + +const allocationRect = { + select: clickable(), + width: attribute('width', '> rect'), + height: attribute('height', '> rect'), + isActive: hasClass('is-active'), + isSelected: hasClass('is-selected'), + running: hasClass('running'), + failed: hasClass('failed'), + pending: hasClass('pending'), +}; + +export default scope => ({ + scope, + + label: text('[data-test-label]'), + statusIcon: attribute('class', '[data-test-status-icon] .icon'), + statusIconLabel: attribute('aria-label', '[data-test-status-icon]'), + + selectNode: clickable('[data-test-node-background]'), + nodeIsInteractive: hasClass('is-interactive', '[data-test-node-background]'), + nodeIsSelected: hasClass('is-selected', '[data-test-node-background]'), + + memoryRects: collection('[data-test-memory-rect]', { + ...allocationRect, + id: attribute('data-test-memory-rect'), + }), + cpuRects: collection('[data-test-cpu-rect]', { + ...allocationRect, + id: attribute('data-test-cpu-rect'), + }), + + emptyMessage: text('[data-test-empty-message]'), +}); From 64fc738733ea3a7853c47c9a93d058af49de60f2 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 12 Oct 2020 15:26:54 -0700 Subject: [PATCH 37/52] Migrate to the new resources properties for allocs and nodes --- ui/app/serializers/allocation.js | 26 ++++++++-- ui/app/serializers/node.js | 2 + ui/app/serializers/resources.js | 23 ++++++--- ui/mirage/common.js | 15 +++--- ui/mirage/factories/allocation.js | 48 ++++++++----------- ui/mirage/factories/node.js | 2 +- ui/mirage/factories/task-resource.js | 8 ---- ui/mirage/factories/task.js | 12 ++++- ui/mirage/serializers/allocation.js | 20 ++++---- ui/tests/acceptance/allocation-detail-test.js | 8 ++-- ui/tests/acceptance/client-detail-test.js | 4 +- ui/tests/acceptance/plugin-detail-test.js | 4 +- ui/tests/acceptance/task-group-detail-test.js | 8 ++-- ui/tests/acceptance/volume-detail-test.js | 4 +- 14 files changed, 107 insertions(+), 77 deletions(-) diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 15cccb6e3..1946bb981 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -9,6 +9,21 @@ const taskGroupFromJob = (job, taskGroupName) => { return taskGroup ? taskGroup : null; }; +const merge = tasks => { + const mergedResources = { + Cpu: { CpuShares: 0 }, + Memory: { MemoryMB: 0 }, + Disk: { DiskMB: 0 }, + }; + + return tasks.reduce((resources, task) => { + resources.Cpu.CpuShares += (task.Cpu && task.Cpu.CpuShares) || 0; + resources.Memory.MemoryMB += (task.Memory && task.Memory.MemoryMB) || 0; + resources.Disk.DiskMB += (task.Disk && task.Disk.DiskMB) || 0; + return resources; + }, mergedResources); +}; + @classic export default class AllocationSerializer extends ApplicationSerializer { @service system; @@ -30,7 +45,7 @@ export default class AllocationSerializer extends ApplicationSerializer { const state = states[key] || {}; const summary = { Name: key }; Object.keys(state).forEach(stateKey => (summary[stateKey] = state[stateKey])); - summary.Resources = hash.TaskResources && hash.TaskResources[key]; + summary.Resources = hash.AllocatedResources && hash.AllocatedResources.Tasks[key]; return summary; }); @@ -57,8 +72,13 @@ export default class AllocationSerializer extends ApplicationSerializer { hash.PreemptedByAllocationID = hash.PreemptedByAllocation || null; hash.WasPreempted = !!hash.PreemptedByAllocationID; - // When present, the resources are nested under AllocatedResources.Shared - hash.AllocatedResources = hash.AllocatedResources && hash.AllocatedResources.Shared; + const shared = hash.AllocatedResources && hash.AllocatedResources.Shared; + hash.AllocatedResources = + hash.AllocatedResources && merge(Object.values(hash.AllocatedResources.Tasks)); + if (shared) { + hash.AllocatedResources.Ports = shared.Ports; + hash.AllocatedResources.Networks = shared.Networks; + } // The Job definition for an allocation is only included in findRecord responses. hash.AllocationTaskGroup = !hash.Job ? null : taskGroupFromJob(hash.Job, hash.TaskGroup); diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js index 6a07ba0ac..a689c40b2 100644 --- a/ui/app/serializers/node.js +++ b/ui/app/serializers/node.js @@ -7,6 +7,8 @@ export default class NodeSerializer extends ApplicationSerializer { attrs = { isDraining: 'Drain', httpAddr: 'HTTPAddr', + resources: 'NodeResources', + reserved: 'ReservedResources', }; mapToArray = ['Drivers', 'HostVolumes']; diff --git a/ui/app/serializers/resources.js b/ui/app/serializers/resources.js index cd642d643..c82e00996 100644 --- a/ui/app/serializers/resources.js +++ b/ui/app/serializers/resources.js @@ -1,12 +1,21 @@ import ApplicationSerializer from './application'; export default class ResourcesSerializer extends ApplicationSerializer { - attrs = { - cpu: 'CPU', - memory: 'MemoryMB', - disk: 'DiskMB', - iops: 'IOPS', - }; + arrayNullOverrides = ['Ports', 'Networks']; - arrayNullOverrides = ['Ports']; + normalize(typeHash, hash) { + hash.Cpu = hash.Cpu && hash.Cpu.CpuShares; + hash.Memory = hash.Memory && hash.Memory.MemoryMB; + hash.Disk = hash.Disk && hash.Disk.DiskMB; + + // Networks for ReservedResources is different than for Resources. + // This smooths over the differences, but doesn't actually support + // anything in the ReservedResources.Networks object, since we don't + // use any of it in the UI. + if (!(hash.Networks instanceof Array)) { + hash.Networks = []; + } + + return super.normalize(...arguments); + } } diff --git a/ui/mirage/common.js b/ui/mirage/common.js index acfd6069d..cac53cdb0 100644 --- a/ui/mirage/common.js +++ b/ui/mirage/common.js @@ -5,10 +5,8 @@ import { provide } from './utils'; const CPU_RESERVATIONS = [250, 500, 1000, 2000, 2500, 4000]; const MEMORY_RESERVATIONS = [256, 512, 1024, 2048, 4096, 8192]; const DISK_RESERVATIONS = [200, 500, 1000, 2000, 5000, 10000, 100000]; -const IOPS_RESERVATIONS = [100000, 250000, 500000, 1000000, 10000000, 20000000]; // There is also a good chance that certain resource restrictions are unbounded -IOPS_RESERVATIONS.push(...Array(1000).fill(0)); DISK_RESERVATIONS.push(...Array(500).fill(0)); const NETWORK_MODES = ['bridge', 'host']; @@ -27,10 +25,15 @@ export const STORAGE_PROVIDERS = ['ebs', 'zfs', 'nfs', 'cow', 'moo']; export function generateResources(options = {}) { return { - CPU: options.CPU || faker.helpers.randomize(CPU_RESERVATIONS), - MemoryMB: options.MemoryMB || faker.helpers.randomize(MEMORY_RESERVATIONS), - DiskMB: options.DiskMB || faker.helpers.randomize(DISK_RESERVATIONS), - IOPS: options.IOPS || faker.helpers.randomize(IOPS_RESERVATIONS), + Cpu: { + CpuShares: options.CPU || faker.helpers.randomize(CPU_RESERVATIONS), + }, + Memory: { + MemoryMB: options.MemoryMB || faker.helpers.randomize(MEMORY_RESERVATIONS), + }, + Disk: { + DiskMB: options.DiskMB || faker.helpers.randomize(DISK_RESERVATIONS), + }, Networks: generateNetworks(options.networks), Ports: generatePorts(options.networks), }; diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index c901a08ca..d433340bc 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -42,15 +42,16 @@ export default Factory.extend({ const taskGroup = server.db.taskGroups.findBy({ name: allocation.taskGroup }); const resources = taskGroup.taskIds.map(id => { const task = server.db.tasks.find(id); - return server.create( - 'task-resource', - { - allocation, - name: task.name, - resources: task.Resources, - }, - 'withReservedPorts' - ); + return server.create('task-resource', { + allocation, + name: task.name, + resources: generateResources({ + CPU: task.resources.CPU, + MemoryMB: task.resources.MemoryMB, + DiskMB: task.resources.DiskMB, + networks: { minPorts: 1 }, + }), + }); }); allocation.update({ taskResourceIds: resources.mapBy('id') }); @@ -62,29 +63,22 @@ export default Factory.extend({ const taskGroup = server.db.taskGroups.findBy({ name: allocation.taskGroup }); const resources = taskGroup.taskIds.map(id => { const task = server.db.tasks.find(id); - return server.create( - 'task-resource', - { - allocation, - name: task.name, - resources: task.Resources, - }, - 'withoutReservedPorts' - ); + return server.create('task-resource', { + allocation, + name: task.name, + resources: generateResources({ + CPU: task.resources.CPU, + MemoryMB: task.resources.MemoryMB, + DiskMB: task.resources.DiskMB, + networks: { minPorts: 0, maxPorts: 0 }, + }), + }); }); allocation.update({ taskResourceIds: resources.mapBy('id') }); }, }), - withAllocatedResources: trait({ - allocatedResources: () => { - return { - Shared: generateResources({ networks: { minPorts: 2 } }), - }; - }, - }), - rescheduleAttempts: 0, rescheduleSuccess: false, @@ -200,7 +194,7 @@ export default Factory.extend({ return server.create('task-resource', { allocation, name: task.name, - resources: task.Resources, + resources: task.originalResources, }); }); diff --git a/ui/mirage/factories/node.js b/ui/mirage/factories/node.js index 03d6427d6..151536e18 100644 --- a/ui/mirage/factories/node.js +++ b/ui/mirage/factories/node.js @@ -74,7 +74,7 @@ export default Factory.extend({ hostVolumes: makeHostVolumes, - resources: generateResources, + nodeResources: generateResources, attributes() { // TODO add variability to these diff --git a/ui/mirage/factories/task-resource.js b/ui/mirage/factories/task-resource.js index 782988bcd..708cc761a 100644 --- a/ui/mirage/factories/task-resource.js +++ b/ui/mirage/factories/task-resource.js @@ -5,12 +5,4 @@ export default Factory.extend({ name: () => '!!!this should be set by the allocation that owns this task state!!!', resources: generateResources, - - withReservedPorts: trait({ - resources: () => generateResources({ networks: { minPorts: 1 } }), - }), - - withoutReservedPorts: trait({ - resources: () => generateResources({ networks: { minPorts: 0, maxPorts: 0 } }), - }), }); diff --git a/ui/mirage/factories/task.js b/ui/mirage/factories/task.js index 6b6759c07..ab3ee6359 100644 --- a/ui/mirage/factories/task.js +++ b/ui/mirage/factories/task.js @@ -16,7 +16,17 @@ export default Factory.extend({ name: id => `task-${faker.hacker.noun().dasherize()}-${id}`, driver: () => faker.helpers.randomize(DRIVERS), - Resources: generateResources, + originalResources: generateResources, + resources: function() { + // Generate resources the usual way, but transform to the old + // shape because that's what the job spec uses. + const resources = this.originalResources; + return { + CPU: resources.Cpu.CpuShares, + MemoryMB: resources.Memory.MemoryMB, + DiskMB: resources.Disk.DiskMB, + }; + }, Lifecycle: i => { const cycle = i % 5; diff --git a/ui/mirage/serializers/allocation.js b/ui/mirage/serializers/allocation.js index eeaaf28f6..d2b58d4bb 100644 --- a/ui/mirage/serializers/allocation.js +++ b/ui/mirage/serializers/allocation.js @@ -18,14 +18,14 @@ export default ApplicationSerializer.extend({ function serializeAllocation(allocation) { allocation.TaskStates = allocation.TaskStates.reduce(arrToObj('Name'), {}); - allocation.Resources = allocation.TaskResources.mapBy('Resources').reduce( - (hash, resources) => { - ['CPU', 'DiskMB', 'IOPS', 'MemoryMB'].forEach(key => (hash[key] += resources[key])); - hash.Networks = resources.Networks; - hash.Ports = resources.Ports; - return hash; - }, - { CPU: 0, DiskMB: 0, IOPS: 0, MemoryMB: 0 } - ); - allocation.TaskResources = allocation.TaskResources.reduce(arrToObj('Name', 'Resources'), {}); + const { Ports, Networks } = allocation.TaskResources[0] + ? allocation.TaskResources[0].Resources + : {}; + allocation.AllocatedResources = { + Shared: { Ports, Networks }, + Tasks: allocation.TaskResources.map(({ Name, Resources }) => ({ Name, ...Resources })).reduce( + arrToObj('Name'), + {} + ), + }; } diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index d4788d88e..83fa23898 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -26,7 +26,7 @@ module('Acceptance | allocation detail', function(hooks) { withGroupServices: true, createAllocations: false, }); - allocation = server.create('allocation', 'withTaskWithPorts', 'withAllocatedResources', { + allocation = server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running', }); @@ -87,7 +87,7 @@ module('Acceptance | allocation detail', function(hooks) { createAllocations: false, }); - const allocation = server.create('allocation', 'withTaskWithPorts', 'withAllocatedResources', { + const allocation = server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running', jobId: job.id, }); @@ -188,7 +188,7 @@ module('Acceptance | allocation detail', function(hooks) { createAllocations: false, }); - allocation = server.create('allocation', 'withTaskWithPorts', 'withAllocatedResources', { + allocation = server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running', jobId: job.id, }); @@ -216,7 +216,7 @@ module('Acceptance | allocation detail', function(hooks) { }); test('ports are listed', async function(assert) { - const allServerPorts = allocation.allocatedResources.Shared.Ports; + const allServerPorts = allocation.taskResources.models[0].resources.Ports; allServerPorts.sortBy('Label').forEach((serverPort, index) => { const renderedPort = Allocation.ports[index]; diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js index 9e95141f1..1ce46d80d 100644 --- a/ui/tests/acceptance/client-detail-test.js +++ b/ui/tests/acceptance/client-detail-test.js @@ -134,8 +134,8 @@ module('Acceptance | client detail', function(hooks) { }); const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); - const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0); - const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0); + const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0); + const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0); await ClientDetail.visit({ id: node.id }); diff --git a/ui/tests/acceptance/plugin-detail-test.js b/ui/tests/acceptance/plugin-detail-test.js index 0f3db7ccc..13eb29b04 100644 --- a/ui/tests/acceptance/plugin-detail-test.js +++ b/ui/tests/acceptance/plugin-detail-test.js @@ -94,8 +94,8 @@ module('Acceptance | plugin detail', function(hooks) { }); const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); - const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0); - const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0); + const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0); + const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0); await PluginDetail.visit({ id: plugin.id }); diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js index a38508f20..af4f240e1 100644 --- a/ui/tests/acceptance/task-group-detail-test.js +++ b/ui/tests/acceptance/task-group-detail-test.js @@ -74,8 +74,8 @@ module('Acceptance | task group detail', function(hooks) { }); test('/jobs/:id/:task-group should list high-level metrics for the allocation', async function(assert) { - const totalCPU = tasks.mapBy('Resources.CPU').reduce(sum, 0); - const totalMemory = tasks.mapBy('Resources.MemoryMB').reduce(sum, 0); + const totalCPU = tasks.mapBy('resources.CPU').reduce(sum, 0); + const totalMemory = tasks.mapBy('resources.MemoryMB').reduce(sum, 0); const totalDisk = taskGroup.ephemeralDisk.SizeMB; await TaskGroup.visit({ id: job.id, name: taskGroup.name }); @@ -199,8 +199,8 @@ module('Acceptance | task group detail', function(hooks) { const allocStats = server.db.clientAllocationStats.find(allocation.id); const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); - const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0); - const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0); + const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0); + const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0); assert.equal( allocationRow.cpu, diff --git a/ui/tests/acceptance/volume-detail-test.js b/ui/tests/acceptance/volume-detail-test.js index b951d54d0..c277dc4bb 100644 --- a/ui/tests/acceptance/volume-detail-test.js +++ b/ui/tests/acceptance/volume-detail-test.js @@ -106,8 +106,8 @@ module('Acceptance | volume detail', function(hooks) { }); const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); - const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0); - const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0); + const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0); + const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0); await VolumeDetail.visit({ id: volume.id }); From 3c398951681e34ed1d6b2a40f39f2cf46d5795a2 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 12 Oct 2020 15:27:37 -0700 Subject: [PATCH 38/52] Update topo viz code to use new alloc/node resources pattern --- ui/app/components/topo-viz.js | 8 ++++---- ui/app/routes/topology.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index b01fe0315..14cb43ccb 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -63,10 +63,10 @@ export default class TopoViz extends Component { node, jobId, groupKey: JSON.stringify([jobId, allocation.taskGroupName]), - memory: allocation.resources.memory, - cpu: allocation.resources.cpu, - memoryPercent: allocation.resources.memory / node.memory, - cpuPercent: allocation.resources.cpu / node.cpu, + memory: allocation.allocatedResources.memory, + cpu: allocation.allocatedResources.cpu, + memoryPercent: allocation.allocatedResources.memory / node.memory, + cpuPercent: allocation.allocatedResources.cpu / node.cpu, isSelected: false, }; } diff --git a/ui/app/routes/topology.js b/ui/app/routes/topology.js index 97b4f0217..84875ceae 100644 --- a/ui/app/routes/topology.js +++ b/ui/app/routes/topology.js @@ -19,9 +19,9 @@ export default class TopologyRoute extends Route.extend(WithForbiddenState) { model() { return RSVP.hash({ - allocations: this.store.findAll('allocation'), jobs: this.store.findAll('job'), - nodes: this.store.findAll('node'), + allocations: this.store.query('allocation', { resources: true }), + nodes: this.store.query('node', { resources: true }), }).catch(notifyForbidden(this)); } } From f918987d51501b0bcee8f752a212a19c3b512471 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 12 Oct 2020 15:27:51 -0700 Subject: [PATCH 39/52] Unit test for for GiB in format-bytes --- ui/tests/unit/helpers/format-bytes-test.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/tests/unit/helpers/format-bytes-test.js b/ui/tests/unit/helpers/format-bytes-test.js index 14da4d13e..b837d31be 100644 --- a/ui/tests/unit/helpers/format-bytes-test.js +++ b/ui/tests/unit/helpers/format-bytes-test.js @@ -24,8 +24,13 @@ module('Unit | Helper | format-bytes', function() { assert.equal(formatBytes([128974848]), '123 MiB'); }); - test('formats x > 1024 * 1024 * 1024 as MiB, since it is the highest allowed unit', function(assert) { - assert.equal(formatBytes([1024 * 1024 * 1024]), '1024 MiB'); - assert.equal(formatBytes([1024 * 1024 * 1024 * 4]), '4096 MiB'); + test('formats 1024 * 1024 * 1024 <= x < 1024 * 1024 * 1024 * 1024 as GiB', function(assert) { + assert.equal(formatBytes([1024 * 1024 * 1024]), '1 GiB'); + assert.equal(formatBytes([1024 * 1024 * 1024 * 4]), '4 GiB'); + }); + + test('formats x > 1024 * 1024 * 1024 * 1024 as GiB, since it is the highest allowed unit', function(assert) { + assert.equal(formatBytes([1024 * 1024 * 1024 * 1024]), '1024 GiB'); + assert.equal(formatBytes([1024 * 1024 * 1024 * 1024 * 4]), '4096 GiB'); }); }); From 9fc33a1f9b002403384738902d17413ec5bd497a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 13 Oct 2020 07:53:05 -0700 Subject: [PATCH 40/52] Adjust topo viz controller to new resource code --- ui/app/controllers/topology.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index f2056ee4d..5ca950f62 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -38,17 +38,19 @@ export default class TopologyControllers extends Controller { return reduceToLargestUnit(this.totalMemory)[1]; } - @computed('model.allocations.@each.resources') + @computed('model.allocations.@each.allocatedResources') get totalReservedMemory() { const mibs = this.model.allocations - .mapBy('resources.memory') + .mapBy('allocatedResources.memory') .reduce((sum, memory) => sum + (memory || 0), 0); return mibs * 1024 * 1024; } - @computed('model.allocations.@each.resources') + @computed('model.allocations.@each.allocatedResources') get totalReservedCPU() { - return this.model.allocations.mapBy('resources.cpu').reduce((sum, cpu) => sum + (cpu || 0), 0); + return this.model.allocations + .mapBy('allocatedResources.cpu') + .reduce((sum, cpu) => sum + (cpu || 0), 0); } @computed('totalMemory', 'totalReservedMemory') @@ -105,11 +107,12 @@ export default class TopologyControllers extends Controller { } @action - setAllocation(allocation) { - this.set('activeAllocation', allocation); + async setAllocation(allocation) { if (allocation) { - allocation.reload(); + await allocation.reload(); + await allocation.job.reload(); } + this.set('activeAllocation', allocation); } @action From 4f537c85115e04efa4ae0cd91f706e7bfbd3284d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 13 Oct 2020 07:54:07 -0700 Subject: [PATCH 41/52] Update scenarios to use new resources code --- ui/mirage/factories/task-group.js | 2 +- ui/mirage/scenarios/topo.js | 70 ++++++++----------------------- 2 files changed, 19 insertions(+), 53 deletions(-) diff --git a/ui/mirage/factories/task-group.js b/ui/mirage/factories/task-group.js index 8e3cbc9ac..73ecf36f6 100644 --- a/ui/mirage/factories/task-group.js +++ b/ui/mirage/factories/task-group.js @@ -79,7 +79,7 @@ export default Factory.extend({ const maybeResources = {}; if (resources) { - maybeResources.Resources = generateResources(resources[idx]); + maybeResources.originalResources = generateResources(resources[idx]); } return server.create('task', { taskGroup: group, diff --git a/ui/mirage/scenarios/topo.js b/ui/mirage/scenarios/topo.js index b2a2d2aec..ed85f0f70 100644 --- a/ui/mirage/scenarios/topo.js +++ b/ui/mirage/scenarios/topo.js @@ -1,19 +1,20 @@ import faker from 'nomad-ui/mirage/faker'; import { generateNetworks, generatePorts } from '../common'; +const genResources = (CPU, Memory) => ({ + Cpu: { CpuShares: CPU }, + Memory: { MemoryMB: Memory }, + Disk: { DiskMB: 10000 }, + Networks: generateNetworks(), + Ports: generatePorts(), +}); + export function topoSmall(server) { server.createList('agent', 3); server.createList('node', 12, { datacenter: 'dc1', status: 'ready', - resources: { - CPU: 3000, - MemoryMB: 5192, - DiskMB: 10000, - IOPS: 100000, - Networks: generateNetworks(), - Ports: generatePorts(), - }, + nodeResources: genResources(3000, 5192), }); const jobResources = [ @@ -48,66 +49,31 @@ export function topoSmallProblems(server) {} export function topoMedium(server) { server.createList('agent', 3); - server.createList('node', 7, { + server.createList('node', 10, { datacenter: 'us-west-1', status: 'ready', - resources: { - CPU: 3000, - MemoryMB: 5192, - DiskMB: 10000, - IOPS: 100000, - Networks: generateNetworks(), - Ports: generatePorts(), - }, + nodeResources: genResources(3000, 5192), }); - server.createList('node', 7, { + server.createList('node', 12, { datacenter: 'us-east-1', status: 'ready', - resources: { - CPU: 3000, - MemoryMB: 5192, - DiskMB: 10000, - IOPS: 100000, - Networks: generateNetworks(), - Ports: generatePorts(), - }, + nodeResources: genResources(3000, 5192), }); - server.createList('node', 7, { + server.createList('node', 11, { datacenter: 'eu-west-1', status: 'ready', - resources: { - CPU: 3000, - MemoryMB: 5192, - DiskMB: 10000, - IOPS: 100000, - Networks: generateNetworks(), - Ports: generatePorts(), - }, + nodeResources: genResources(3000, 5192), }); server.createList('node', 8, { datacenter: 'us-west-1', status: 'ready', - resources: { - CPU: 8000, - MemoryMB: 12192, - DiskMB: 10000, - IOPS: 100000, - Networks: generateNetworks(), - Ports: generatePorts(), - }, + nodeResources: genResources(8000, 12192), }); - server.createList('node', 7, { + server.createList('node', 9, { datacenter: 'us-east-1', status: 'ready', - resources: { - CPU: 8000, - MemoryMB: 12192, - DiskMB: 10000, - IOPS: 100000, - Networks: generateNetworks(), - Ports: generatePorts(), - }, + nodeResources: genResources(8000, 12192), }); const jobResources = [ From 8b96667a61239ee8132817c08b4ee9aa9cabbbd2 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 13 Oct 2020 17:27:11 -0700 Subject: [PATCH 42/52] Remove temp reloading of nodes in the TopoViz component --- ui/app/components/topo-viz.js | 14 ++----- ui/app/templates/components/topo-viz.hbs | 52 ++++++++++-------------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 14cb43ccb..eea9b2c84 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -71,14 +71,11 @@ export default class TopoViz extends Component { }; } - @task(function*() { + @action + buildTopology() { const nodes = this.args.nodes; const allocations = this.args.allocations; - // Nodes are probably partials and we'll need the resources on them - // TODO: this is an API update waiting to happen. - yield RSVP.all(nodes.map(node => (node.isPartial ? node.reload() : RSVP.resolve(node)))); - // Wrap nodes in a topo viz specific data structure and build an index to speed up allocation assignment const nodeContainers = []; const nodeIndex = {}; @@ -95,7 +92,7 @@ export default class TopoViz extends Component { const nodeId = allocation.belongsTo('node').id(); const nodeContainer = nodeIndex[nodeId]; if (!nodeContainer) - throw new Error(`Node ${nodeId} for alloc ${allocation.id} not in index???`); + throw new Error(`Node ${nodeId} for alloc ${allocation.id} not in index.`); const allocationContainer = this.dataForAllocation(allocation, nodeContainer); nodeContainer.allocations.push(allocationContainer); @@ -126,14 +123,12 @@ export default class TopoViz extends Component { .domain(extent(nodeContainers.mapBy('memory'))), }; this.topology = topology; - }) - buildTopology; + } @action async loadNodes() { await RSVP.all(this.args.nodes.map(node => node.reload())); - // TODO: Make the range dynamic based on the extent of the domain this.heightScale = scaleLinear() .range([15, 40]) .domain(extent(this.args.nodes.map(node => node.resources.memory))); @@ -207,7 +202,6 @@ export default class TopoViz extends Component { computedActiveEdges() { // Wait a render cycle run.next(() => { - // const path = line().curve(curveCardinal.tension(0.5)); const path = line().curve(curveBasis); // 1. Get the active element const allocation = this.activeAllocation.allocation; diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 8648ab396..1722842d5 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,33 +1,25 @@ -
- {{#if this.buildTopology.isRunning}} -
-

Loading. If you have a lot of clients this may take awhile

-

Every client needs to be loaded individually. This is a shortcoming of the prototype and will be fixed before this is graduated to the actual Nomad project.

- -
- {{else}} - - - +
+ + + - {{#if this.activeAllocation}} - - - {{#each this.activeEdges as |edge|}} - - {{/each}} - - - {{/if}} + {{#if this.activeAllocation}} + + + {{#each this.activeEdges as |edge|}} + + {{/each}} + + {{/if}}
From c8ea0196298bc0fa15798d8eb66477f1ad1f5f6d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 14 Oct 2020 00:54:39 -0700 Subject: [PATCH 43/52] Test coverage for TopoViz::Datacenter --- ui/app/components/topo-viz.js | 1 - ui/app/components/topo-viz/datacenter.js | 22 +-- ui/app/templates/components/topo-viz.hbs | 5 +- .../components/topo-viz/datacenter.hbs | 4 +- ui/app/templates/components/topo-viz/node.hbs | 2 +- .../components/topo-viz/datacenter-test.js | 160 ++++++++++++++++++ .../pages/components/topo-viz/datacenter.js | 9 + ui/tests/pages/components/topo-viz/node.js | 3 +- 8 files changed, 184 insertions(+), 22 deletions(-) create mode 100644 ui/tests/integration/components/topo-viz/datacenter-test.js create mode 100644 ui/tests/pages/components/topo-viz/datacenter.js diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index eea9b2c84..6b9e98e72 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -2,7 +2,6 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action, set } from '@ember/object'; import { run } from '@ember/runloop'; -import { task } from 'ember-concurrency'; import { scaleLinear } from 'd3-scale'; import { extent, deviation, mean } from 'd3-array'; import { line, curveBasis } from 'd3-shape'; diff --git a/ui/app/components/topo-viz/datacenter.js b/ui/app/components/topo-viz/datacenter.js index 7ebe7f66b..0750fc1eb 100644 --- a/ui/app/components/topo-viz/datacenter.js +++ b/ui/app/components/topo-viz/datacenter.js @@ -1,10 +1,12 @@ import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; export default class TopoVizDatacenter extends Component { - @tracked scheduledAllocations = []; - @tracked aggregatedNodeResources = { cpu: 0, memory: 0 }; + get scheduledAllocations() { + return this.args.datacenter.nodes.reduce( + (all, node) => all.concat(node.allocations.filterBy('allocation.isScheduled')), + [] + ); + } get aggregatedAllocationResources() { return this.scheduledAllocations.reduce( @@ -17,14 +19,8 @@ export default class TopoVizDatacenter extends Component { ); } - @action - loadAllocations() { - this.scheduledAllocations = this.args.datacenter.nodes.reduce( - (all, node) => all.concat(node.allocations.filterBy('allocation.isScheduled')), - [] - ); - - this.aggregatedNodeResources = this.args.datacenter.nodes.reduce( + get aggregatedNodeResources() { + return this.args.datacenter.nodes.reduce( (totals, node) => { totals.cpu += node.cpu; totals.memory += node.memory; @@ -32,7 +28,5 @@ export default class TopoVizDatacenter extends Component { }, { cpu: 0, memory: 0 } ); - - this.args.onLoad && this.args.onLoad(); } } diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 1722842d5..7a367a3a5 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -2,15 +2,14 @@ + @withSpacing={{true}} as |dc|> + @onNodeSelect={{this.showNodeDetails}} /> {{#if this.activeAllocation}} diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index 2798fbe91..0a50a1f5a 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -1,5 +1,5 @@ -
-
+
+
{{@datacenter.name}} {{this.scheduledAllocations.length}} Allocs {{@datacenter.nodes.length}} Nodes diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 0e4eb98cd..fc02614a2 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,4 +1,4 @@ -
+
{{#unless @isDense}}

{{#if @node.node.isDraining}} diff --git a/ui/tests/integration/components/topo-viz/datacenter-test.js b/ui/tests/integration/components/topo-viz/datacenter-test.js new file mode 100644 index 000000000..3480aae88 --- /dev/null +++ b/ui/tests/integration/components/topo-viz/datacenter-test.js @@ -0,0 +1,160 @@ +import { find } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; +import { create } from 'ember-cli-page-object'; +import sinon from 'sinon'; +import faker from 'nomad-ui/mirage/faker'; +import topoVizDatacenterPageObject from 'nomad-ui/tests/pages/components/topo-viz/datacenter'; + +const TopoVizDatacenter = create(topoVizDatacenterPageObject()); + +const nodeGen = (name, datacenter, memory, cpu, allocations = []) => ({ + datacenter, + memory, + cpu, + node: { name }, + allocations: allocations.map(alloc => ({ + memory: alloc.memory, + cpu: alloc.cpu, + memoryPercent: alloc.memory / memory, + cpuPercent: alloc.cpu / cpu, + allocation: { + id: faker.random.uuid(), + isScheduled: true, + }, + })), +}); + +// Used in Array#reduce to sum by a property common to an array of objects +const sumBy = prop => (sum, obj) => (sum += obj[prop]); + +module('Integration | Component | TopoViz::Datacenter', function(hooks) { + setupRenderingTest(hooks); + + const commonProps = props => ({ + isSingleColumn: true, + isDense: false, + heightScale: () => 50, + onAllocationSelect: sinon.spy(), + onNodeSelect: sinon.spy(), + ...props, + }); + + const commonTemplate = hbs` + + `; + + test('presents as a div with a label and a FlexMasonry with a collection of nodes', async function(assert) { + this.setProperties( + commonProps({ + datacenter: { + name: 'dc1', + nodes: [nodeGen('node-1', 'dc1', 1000, 500)], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(TopoVizDatacenter.isPresent); + assert.equal(TopoVizDatacenter.nodes.length, this.datacenter.nodes.length); + + await componentA11yAudit(this.element, assert); + }); + + test('datacenter stats are an aggregate of node stats', async function(assert) { + this.setProperties( + commonProps({ + datacenter: { + name: 'dc1', + nodes: [ + nodeGen('node-1', 'dc1', 1000, 500, [ + { memory: 100, cpu: 300 }, + { memory: 200, cpu: 50 }, + ]), + nodeGen('node-2', 'dc1', 1500, 100, [ + { memory: 50, cpu: 80 }, + { memory: 100, cpu: 20 }, + ]), + nodeGen('node-3', 'dc1', 2000, 300), + nodeGen('node-4', 'dc1', 3000, 200), + ], + }, + }) + ); + + await this.render(commonTemplate); + + const allocs = this.datacenter.nodes.reduce( + (allocs, node) => allocs.concat(node.allocations), + [] + ); + const memoryReserved = allocs.reduce(sumBy('memory'), 0); + const cpuReserved = allocs.reduce(sumBy('cpu'), 0); + const memoryTotal = this.datacenter.nodes.reduce(sumBy('memory'), 0); + const cpuTotal = this.datacenter.nodes.reduce(sumBy('cpu'), 0); + + assert.ok(TopoVizDatacenter.label.includes(this.datacenter.name)); + assert.ok(TopoVizDatacenter.label.includes(`${this.datacenter.nodes.length} Nodes`)); + assert.ok(TopoVizDatacenter.label.includes(`${allocs.length} Allocs`)); + assert.ok(TopoVizDatacenter.label.includes(`${memoryReserved}/${memoryTotal} MiB`)); + assert.ok(TopoVizDatacenter.label.includes(`${cpuReserved}/${cpuTotal} Mhz`)); + }); + + test('when @isSingleColumn is true, the FlexMasonry layout gets one column, otherwise it gets two', async function(assert) { + this.setProperties( + commonProps({ + isSingleColumn: true, + datacenter: { + name: 'dc1', + nodes: [nodeGen('node-1', 'dc1', 1000, 500), nodeGen('node-2', 'dc1', 1000, 500)], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(find('[data-test-flex-masonry].flex-masonry-columns-1')); + + this.set('isSingleColumn', false); + assert.ok(find('[data-test-flex-masonry].flex-masonry-columns-2')); + }); + + test('args get passed down to the TopViz::Node children', async function(assert) { + const heightSpy = sinon.spy(); + this.setProperties( + commonProps({ + isDense: true, + heightScale: (...args) => { + heightSpy(...args); + return 50; + }, + datacenter: { + name: 'dc1', + nodes: [nodeGen('node-1', 'dc1', 1000, 500, [{ memory: 100, cpu: 300 }])], + }, + }) + ); + + await this.render(commonTemplate); + + TopoVizDatacenter.nodes[0].as(async TopoVizNode => { + assert.notOk(TopoVizNode.labelIsPresent); + assert.ok(heightSpy.calledWith(this.datacenter.nodes[0].memory)); + + await TopoVizNode.selectNode(); + assert.ok(this.onNodeSelect.calledWith(this.datacenter.nodes[0])); + + await TopoVizNode.memoryRects[0].select(); + assert.ok(this.onAllocationSelect.calledWith(this.datacenter.nodes[0].allocations[0])); + }); + }); +}); diff --git a/ui/tests/pages/components/topo-viz/datacenter.js b/ui/tests/pages/components/topo-viz/datacenter.js new file mode 100644 index 000000000..1388eeead --- /dev/null +++ b/ui/tests/pages/components/topo-viz/datacenter.js @@ -0,0 +1,9 @@ +import { collection, text } from 'ember-cli-page-object'; +import TopoVizNode from './node'; + +export default scope => ({ + scope, + + label: text('[data-test-topo-viz-datacenter-label]'), + nodes: collection('[data-test-topo-viz-node]', TopoVizNode()), +}); diff --git a/ui/tests/pages/components/topo-viz/node.js b/ui/tests/pages/components/topo-viz/node.js index 5dac302a7..665940ebf 100644 --- a/ui/tests/pages/components/topo-viz/node.js +++ b/ui/tests/pages/components/topo-viz/node.js @@ -1,4 +1,4 @@ -import { attribute, collection, clickable, hasClass, text } from 'ember-cli-page-object'; +import { attribute, collection, clickable, hasClass, isPresent, text } from 'ember-cli-page-object'; const allocationRect = { select: clickable(), @@ -15,6 +15,7 @@ export default scope => ({ scope, label: text('[data-test-label]'), + labelIsPresent: isPresent('[data-test-label]'), statusIcon: attribute('class', '[data-test-status-icon] .icon'), statusIconLabel: attribute('aria-label', '[data-test-status-icon]'), From a2b57c15eb96536b9abf118a89ca702e7bcb5daf Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 02:18:55 -0700 Subject: [PATCH 44/52] Unit and integration tests for TopoViz component --- ui/app/components/topo-viz.js | 13 -- ui/app/templates/components/topo-viz.hbs | 6 +- ui/tests/helpers/glimmer-factory.js | 25 +++ .../integration/components/topo-viz-test.js | 144 +++++++++++++ ui/tests/pages/components/topo-viz.js | 11 + ui/tests/unit/components/topo-viz-test.js | 191 ++++++++++++++++++ 6 files changed, 374 insertions(+), 16 deletions(-) create mode 100644 ui/tests/helpers/glimmer-factory.js create mode 100644 ui/tests/integration/components/topo-viz-test.js create mode 100644 ui/tests/pages/components/topo-viz.js create mode 100644 ui/tests/unit/components/topo-viz-test.js diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 6b9e98e72..ee3616102 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -5,11 +5,8 @@ import { run } from '@ember/runloop'; import { scaleLinear } from 'd3-scale'; import { extent, deviation, mean } from 'd3-array'; import { line, curveBasis } from 'd3-shape'; -import RSVP from 'rsvp'; export default class TopoViz extends Component { - @tracked heightScale = null; - @tracked isLoaded = false; @tracked element = null; @tracked topology = { datacenters: [] }; @@ -124,16 +121,6 @@ export default class TopoViz extends Component { this.topology = topology; } - @action - async loadNodes() { - await RSVP.all(this.args.nodes.map(node => node.reload())); - - this.heightScale = scaleLinear() - .range([15, 40]) - .domain(extent(this.args.nodes.map(node => node.resources.memory))); - this.isLoaded = true; - } - @action captureElement(element) { this.element = element; diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 7a367a3a5..63d123417 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -1,4 +1,4 @@ -

+
{{#if this.activeAllocation}} - + {{#each this.activeEdges as |edge|}} - + {{/each}} diff --git a/ui/tests/helpers/glimmer-factory.js b/ui/tests/helpers/glimmer-factory.js new file mode 100644 index 000000000..c699f74be --- /dev/null +++ b/ui/tests/helpers/glimmer-factory.js @@ -0,0 +1,25 @@ +// Look up the component class in the glimmer component manager and return a +// function to construct components as if they were functions. +const glimmerComponentInstantiator = (owner, componentKey) => args => { + const componentManager = owner.lookup('component-manager:glimmer'); + const componentClass = owner.factoryFor(`component:${componentKey}`).class; + return componentManager.createComponent(componentClass, { named: args }); +}; + +// Use like +// +// setupGlimmerComponentFactory(hooks, 'my-component') +// +// test('testing my component', function(assert) { +// const component = this.createComponent({ hello: 'world' }); +// assert.equal(component.args.hello, 'world'); +// }); +export default function setupGlimmerComponentFactory(hooks, componentKey) { + hooks.beforeEach(function() { + this.createComponent = glimmerComponentInstantiator(this.owner, componentKey); + }); + + hooks.afterEach(function() { + delete this.createComponent; + }); +} diff --git a/ui/tests/integration/components/topo-viz-test.js b/ui/tests/integration/components/topo-viz-test.js new file mode 100644 index 000000000..5d4ae961f --- /dev/null +++ b/ui/tests/integration/components/topo-viz-test.js @@ -0,0 +1,144 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; +import { create } from 'ember-cli-page-object'; +import sinon from 'sinon'; +import faker from 'nomad-ui/mirage/faker'; +import topoVizPageObject from 'nomad-ui/tests/pages/components/topo-viz'; + +const TopoViz = create(topoVizPageObject()); + +const alloc = (nodeId, jobId, taskGroupName, memory, cpu, props = {}) => ({ + id: faker.random.uuid(), + taskGroupName, + isScheduled: true, + allocatedResources: { + cpu, + memory, + }, + belongsTo: type => ({ + id: () => (type === 'job' ? jobId : nodeId), + }), + ...props, +}); + +const node = (datacenter, id, memory, cpu) => ({ + datacenter, + id, + resources: { memory, cpu }, +}); + +module('Integration | Component | TopoViz', function(hooks) { + setupRenderingTest(hooks); + + const commonTemplate = hbs` + + `; + + test('presents as a FlexMasonry of datacenters', async function(assert) { + this.setProperties({ + nodes: [node('dc1', 'node0', 1000, 500), node('dc2', 'node1', 1000, 500)], + + allocations: [ + alloc('node0', 'job1', 'group', 100, 100), + alloc('node0', 'job1', 'group', 100, 100), + alloc('node1', 'job1', 'group', 100, 100), + ], + }); + + await this.render(commonTemplate); + + assert.equal(TopoViz.datacenters.length, 2); + assert.equal(TopoViz.datacenters[0].nodes.length, 1); + assert.equal(TopoViz.datacenters[1].nodes.length, 1); + assert.equal(TopoViz.datacenters[0].nodes[0].memoryRects.length, 2); + assert.equal(TopoViz.datacenters[1].nodes[0].memoryRects.length, 1); + + await componentA11yAudit(this.element, assert); + }); + + test('clicking on a node in a deeply nested TopoViz::Node will toggle node selection and call @onNodeSelect', async function(assert) { + this.setProperties({ + // TopoViz must be dense for node selection to be a feature + nodes: Array(55) + .fill(null) + .map((_, index) => node('dc1', `node${index}`, 1000, 500)), + allocations: [], + onNodeSelect: sinon.spy(), + }); + + await this.render(commonTemplate); + + await TopoViz.datacenters[0].nodes[0].selectNode(); + assert.ok(this.onNodeSelect.calledOnce); + assert.equal(this.onNodeSelect.getCall(0).args[0].node, this.nodes[0]); + + await TopoViz.datacenters[0].nodes[0].selectNode(); + assert.ok(this.onNodeSelect.calledTwice); + assert.equal(this.onNodeSelect.getCall(1).args[0], null); + }); + + test('clicking on an allocation in a deeply nested TopoViz::Node will update the topology object with selections and call @onAllocationSelect and @onNodeSelect', async function(assert) { + this.setProperties({ + nodes: [node('dc1', 'node0', 1000, 500)], + allocations: [alloc('node0', 'job1', 'group', 100, 100)], + onNodeSelect: sinon.spy(), + onAllocationSelect: sinon.spy(), + }); + + await this.render(commonTemplate); + + await TopoViz.datacenters[0].nodes[0].memoryRects[0].select(); + assert.ok(this.onAllocationSelect.calledOnce); + assert.equal(this.onAllocationSelect.getCall(0).args[0], this.allocations[0]); + assert.ok(this.onNodeSelect.calledOnce); + + await TopoViz.datacenters[0].nodes[0].memoryRects[0].select(); + assert.ok(this.onAllocationSelect.calledTwice); + assert.equal(this.onAllocationSelect.getCall(1).args[0], null); + assert.ok(this.onNodeSelect.calledTwice); + assert.ok(this.onNodeSelect.alwaysCalledWith(null)); + }); + + test('clicking on an allocation in a deeply nested TopoViz::Node will associate sibling allocations with curves', async function(assert) { + this.setProperties({ + nodes: [ + node('dc1', 'node0', 1000, 500), + node('dc1', 'node1', 1000, 500), + node('dc2', 'node2', 1000, 500), + ], + allocations: [ + alloc('node0', 'job1', 'group', 100, 100), + alloc('node0', 'job1', 'group', 100, 100), + alloc('node1', 'job1', 'group', 100, 100), + alloc('node2', 'job1', 'group', 100, 100), + alloc('node0', 'job1', 'groupTwo', 100, 100), + alloc('node1', 'job2', 'group', 100, 100), + alloc('node2', 'job2', 'groupTwo', 100, 100), + ], + onNodeSelect: sinon.spy(), + onAllocationSelect: sinon.spy(), + }); + + const selectedAllocations = this.allocations.filter( + alloc => alloc.belongsTo('job').id() === 'job1' && alloc.taskGroupName === 'group' + ); + + await this.render(commonTemplate); + + assert.notOk(TopoViz.allocationAssociationsArePresent); + + await TopoViz.datacenters[0].nodes[0].memoryRects[0].select(); + + assert.ok(TopoViz.allocationAssociationsArePresent); + assert.equal(TopoViz.allocationAssociations.length, selectedAllocations.length * 2); + + await TopoViz.datacenters[0].nodes[0].memoryRects[0].select(); + assert.notOk(TopoViz.allocationAssociationsArePresent); + }); +}); diff --git a/ui/tests/pages/components/topo-viz.js b/ui/tests/pages/components/topo-viz.js new file mode 100644 index 000000000..657e5a165 --- /dev/null +++ b/ui/tests/pages/components/topo-viz.js @@ -0,0 +1,11 @@ +import { collection, isPresent } from 'ember-cli-page-object'; +import TopoVizDatacenter from './topo-viz/datacenter'; + +export default scope => ({ + scope, + + datacenters: collection('[data-test-topo-viz-datacenter]', TopoVizDatacenter()), + + allocationAssociationsArePresent: isPresent('[data-test-allocation-associations]'), + allocationAssociations: collection('[data-test-allocation-association]'), +}); diff --git a/ui/tests/unit/components/topo-viz-test.js b/ui/tests/unit/components/topo-viz-test.js new file mode 100644 index 000000000..ad4a018ef --- /dev/null +++ b/ui/tests/unit/components/topo-viz-test.js @@ -0,0 +1,191 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import setupGlimmerComponentFactory from 'nomad-ui/tests/helpers/glimmer-factory'; + +module('Unit | Component | TopoViz', function(hooks) { + setupTest(hooks); + setupGlimmerComponentFactory(hooks, 'topo-viz'); + + test('the topology object properly organizes a tree of datacenters > nodes > allocations', async function(assert) { + const nodes = [ + { datacenter: 'dc1', id: 'node0', resources: {} }, + { datacenter: 'dc2', id: 'node1', resources: {} }, + { datacenter: 'dc1', id: 'node2', resources: {} }, + ]; + + const node0Allocs = [ + alloc({ nodeId: 'node0', jobId: 'job0', taskGroupName: 'group' }), + alloc({ nodeId: 'node0', jobId: 'job1', taskGroupName: 'group' }), + ]; + const node1Allocs = [ + alloc({ nodeId: 'node1', jobId: 'job0', taskGroupName: 'group' }), + alloc({ nodeId: 'node1', jobId: 'job1', taskGroupName: 'group' }), + ]; + const node2Allocs = [ + alloc({ nodeId: 'node2', jobId: 'job0', taskGroupName: 'group' }), + alloc({ nodeId: 'node2', jobId: 'job1', taskGroupName: 'group' }), + ]; + + const allocations = [...node0Allocs, ...node1Allocs, ...node2Allocs]; + + const topoViz = this.createComponent({ nodes, allocations }); + + topoViz.buildTopology(); + + assert.deepEqual(topoViz.topology.datacenters.mapBy('name'), ['dc1', 'dc2']); + assert.deepEqual(topoViz.topology.datacenters[0].nodes.mapBy('node'), [nodes[0], nodes[2]]); + assert.deepEqual(topoViz.topology.datacenters[1].nodes.mapBy('node'), [nodes[1]]); + assert.deepEqual( + topoViz.topology.datacenters[0].nodes[0].allocations.mapBy('allocation'), + node0Allocs + ); + assert.deepEqual( + topoViz.topology.datacenters[1].nodes[0].allocations.mapBy('allocation'), + node1Allocs + ); + assert.deepEqual( + topoViz.topology.datacenters[0].nodes[1].allocations.mapBy('allocation'), + node2Allocs + ); + }); + + test('the topology object contains an allocation index keyed by jobId+taskGroupName', async function(assert) { + const allocations = [ + alloc({ nodeId: 'node0', jobId: 'job0', taskGroupName: 'one' }), + alloc({ nodeId: 'node0', jobId: 'job0', taskGroupName: 'one' }), + alloc({ nodeId: 'node0', jobId: 'job0', taskGroupName: 'two' }), + alloc({ nodeId: 'node0', jobId: 'job1', taskGroupName: 'one' }), + alloc({ nodeId: 'node0', jobId: 'job1', taskGroupName: 'two' }), + alloc({ nodeId: 'node0', jobId: 'job1', taskGroupName: 'three' }), + alloc({ nodeId: 'node0', jobId: 'job2', taskGroupName: 'one' }), + alloc({ nodeId: 'node0', jobId: 'job2', taskGroupName: 'one' }), + alloc({ nodeId: 'node0', jobId: 'job2', taskGroupName: 'one' }), + alloc({ nodeId: 'node0', jobId: 'job2', taskGroupName: 'one' }), + ]; + + const nodes = [{ datacenter: 'dc1', id: 'node0', resources: {} }]; + const topoViz = this.createComponent({ nodes, allocations }); + + topoViz.buildTopology(); + + assert.deepEqual( + Object.keys(topoViz.topology.allocationIndex).sort(), + [ + JSON.stringify(['job0', 'one']), + JSON.stringify(['job0', 'two']), + + JSON.stringify(['job1', 'one']), + JSON.stringify(['job1', 'two']), + JSON.stringify(['job1', 'three']), + + JSON.stringify(['job2', 'one']), + ].sort() + ); + + Object.keys(topoViz.topology.allocationIndex).forEach(key => { + const [jobId, group] = JSON.parse(key); + assert.deepEqual( + topoViz.topology.allocationIndex[key].mapBy('allocation'), + allocations.filter(alloc => alloc.jobId === jobId && alloc.taskGroupName === group) + ); + }); + }); + + test('isSingleColumn is true when there is only one datacenter', async function(assert) { + const oneDc = [{ datacenter: 'dc1', id: 'node0', resources: {} }]; + const twoDc = [...oneDc, { datacenter: 'dc2', id: 'node1', resources: {} }]; + + const topoViz1 = this.createComponent({ nodes: oneDc, allocations: [] }); + const topoViz2 = this.createComponent({ nodes: twoDc, allocations: [] }); + + topoViz1.buildTopology(); + topoViz2.buildTopology(); + + assert.ok(topoViz1.isSingleColumn); + assert.notOk(topoViz2.isSingleColumn); + }); + + test('isSingleColumn is true when there are multiple datacenters with a high variance in node count', async function(assert) { + const uniformDcs = [ + { datacenter: 'dc1', id: 'node0', resources: {} }, + { datacenter: 'dc2', id: 'node1', resources: {} }, + ]; + const skewedDcs = [ + { datacenter: 'dc1', id: 'node0', resources: {} }, + { datacenter: 'dc2', id: 'node1', resources: {} }, + { datacenter: 'dc2', id: 'node2', resources: {} }, + { datacenter: 'dc2', id: 'node3', resources: {} }, + { datacenter: 'dc2', id: 'node4', resources: {} }, + ]; + + const twoColumnViz = this.createComponent({ nodes: uniformDcs, allocations: [] }); + const oneColumViz = this.createComponent({ nodes: skewedDcs, allocations: [] }); + + twoColumnViz.buildTopology(); + oneColumViz.buildTopology(); + + assert.notOk(twoColumnViz.isSingleColumn); + assert.ok(oneColumViz.isSingleColumn); + }); + + test('datacenterIsSingleColumn is only ever false when isSingleColumn is false and the total node count is high', async function(assert) { + const manyUniformNodes = Array(25) + .fill(null) + .map((_, index) => ({ + datacenter: index > 12 ? 'dc2' : 'dc1', + id: `node${index}`, + resources: {}, + })); + const manySkewedNodes = Array(25) + .fill(null) + .map((_, index) => ({ + datacenter: index > 5 ? 'dc2' : 'dc1', + id: `node${index}`, + resources: {}, + })); + + const oneColumnViz = this.createComponent({ nodes: manyUniformNodes, allocations: [] }); + const twoColumnViz = this.createComponent({ nodes: manySkewedNodes, allocations: [] }); + + oneColumnViz.buildTopology(); + twoColumnViz.buildTopology(); + + assert.ok(oneColumnViz.datacenterIsSingleColumn); + assert.notOk(oneColumnViz.isSingleColumn); + + assert.notOk(twoColumnViz.datacenterIsSingleColumn); + assert.ok(twoColumnViz.isSingleColumn); + }); + + test('dataForAllocation correctly calculates proportion of node utilization and group key', async function(assert) { + const nodes = [{ datacenter: 'dc1', id: 'node0', resources: { cpu: 100, memory: 250 } }]; + const allocations = [ + alloc({ + nodeId: 'node0', + jobId: 'job0', + taskGroupName: 'group', + allocatedResources: { cpu: 50, memory: 25 }, + }), + ]; + + const topoViz = this.createComponent({ nodes, allocations }); + topoViz.buildTopology(); + + assert.equal(topoViz.topology.datacenters[0].nodes[0].allocations[0].cpuPercent, 0.5); + assert.equal(topoViz.topology.datacenters[0].nodes[0].allocations[0].memoryPercent, 0.1); + }); +}); + +function alloc(props) { + return { + ...props, + allocatedResources: props.allocatedResources || {}, + belongsTo(type) { + return { + id() { + return type === 'job' ? props.jobId : props.nodeId; + }, + }; + }, + }; +} From 97510a839701f973ae2f51463e2c75ee89770c72 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 02:53:46 -0700 Subject: [PATCH 45/52] Some light topology acceptance tests --- ui/app/templates/topology.hbs | 2 +- ui/tests/acceptance/topology-test.js | 53 ++++++++++++++++++++++++++++ ui/tests/pages/topology.js | 11 ++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 ui/tests/acceptance/topology-test.js create mode 100644 ui/tests/pages/topology.js diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index 4d562e0ed..19e627c25 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -24,7 +24,7 @@
-
+
{{#if this.activeNode}}Client{{else if this.activeAllocation}}Allocation{{else}}Cluster{{/if}} Details
diff --git a/ui/tests/acceptance/topology-test.js b/ui/tests/acceptance/topology-test.js new file mode 100644 index 000000000..79ca675a8 --- /dev/null +++ b/ui/tests/acceptance/topology-test.js @@ -0,0 +1,53 @@ +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; +import Topology from 'nomad-ui/tests/pages/topology'; + +// TODO: Once we settle on the contents of the info panel, the contents +// should also get acceptance tests. +module('Acceptance | topology', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function() { + server.create('job', { createAllocations: false }); + }); + + test('it passes an accessibility audit', async function(assert) { + server.createList('node', 3); + server.createList('allocation', 5); + + await Topology.visit(); + await a11yAudit(assert); + }); + + test('by default the info panel shows cluster aggregate stats', async function(assert) { + server.createList('node', 3); + server.createList('allocation', 5); + + await Topology.visit(); + assert.equal(Topology.infoPanelTitle, 'Cluster Details'); + }); + + test('when an allocation is selected, the info panel shows information on the allocation', async function(assert) { + server.createList('node', 1); + server.createList('allocation', 5); + + await Topology.visit(); + + await Topology.viz.datacenters[0].nodes[0].memoryRects[0].select(); + assert.equal(Topology.infoPanelTitle, 'Allocation Details'); + }); + + test('when a node is selected, the info panel shows information on the node', async function(assert) { + // A high node count is required for node selection + server.createList('node', 51); + server.createList('allocation', 5); + + await Topology.visit(); + + await Topology.viz.datacenters[0].nodes[0].selectNode(); + assert.equal(Topology.infoPanelTitle, 'Client Details'); + }); +}); diff --git a/ui/tests/pages/topology.js b/ui/tests/pages/topology.js new file mode 100644 index 000000000..b9f2c63a6 --- /dev/null +++ b/ui/tests/pages/topology.js @@ -0,0 +1,11 @@ +import { create, text, visitable } from 'ember-cli-page-object'; + +import TopoViz from 'nomad-ui/tests/pages/components/topo-viz'; + +export default create({ + visit: visitable('/topology'), + + infoPanelTitle: text('[data-test-info-panel-title]'), + + viz: TopoViz('[data-test-topo-viz]'), +}); From 649873745ff8ca0648b3696173e4a7c3955c784a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 10:30:34 -0700 Subject: [PATCH 46/52] Treat legend term pairs as single wrapping elements --- ui/app/styles/components/legend.scss | 10 ++++++++-- ui/app/templates/topology.hbs | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ui/app/styles/components/legend.scss b/ui/app/styles/components/legend.scss index a1ce69d7f..2e86faf97 100644 --- a/ui/app/styles/components/legend.scss +++ b/ui/app/styles/components/legend.scss @@ -20,8 +20,14 @@ margin-left: 0.5em; } - dd + dt { - margin-left: 1.5em; + .legend-term { + display: inline-block; + whitespace: nowrap; + margin-right: 1.5em; + + &:last-child { + margin-right: 0; + } } } } diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index 19e627c25..19c1fd515 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -16,9 +16,9 @@

Allocation Status

-
Running
-
Failed
-
Starting
+
Running
+
Failed
+
Starting
From 41df088abe80d4081a9ea1a05bf341e4edfdcc74 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 10:32:08 -0700 Subject: [PATCH 47/52] Reset the standard environment values --- ui/config/environment.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/config/environment.js b/ui/config/environment.js index 65af8d9a6..e459f2624 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -24,11 +24,11 @@ module.exports = function(environment) { }, APP: { - blockingQueries: false, - mirageScenario: 'topoSmall', + blockingQueries: true, + mirageScenario: 'topoMedium', mirageWithNamespaces: false, mirageWithTokens: true, - mirageWithRegions: false, + mirageWithRegions: true, }, }; From 56bf526778c08a218aac1070e85356fb340fbf03 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 10:35:23 -0700 Subject: [PATCH 48/52] Describe the glimmer-factory better including the motive --- ui/tests/helpers/glimmer-factory.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ui/tests/helpers/glimmer-factory.js b/ui/tests/helpers/glimmer-factory.js index c699f74be..c9cd865d3 100644 --- a/ui/tests/helpers/glimmer-factory.js +++ b/ui/tests/helpers/glimmer-factory.js @@ -1,11 +1,8 @@ -// Look up the component class in the glimmer component manager and return a -// function to construct components as if they were functions. -const glimmerComponentInstantiator = (owner, componentKey) => args => { - const componentManager = owner.lookup('component-manager:glimmer'); - const componentClass = owner.factoryFor(`component:${componentKey}`).class; - return componentManager.createComponent(componentClass, { named: args }); -}; - +// Used in glimmer component unit tests. Glimmer components should typically +// be tested with integration tests, but occasionally individual methods or +// properties have logic that isn't coupled to rendering or the DOM and can +// be better tested in a unit fashion. +// // Use like // // setupGlimmerComponentFactory(hooks, 'my-component') @@ -23,3 +20,13 @@ export default function setupGlimmerComponentFactory(hooks, componentKey) { delete this.createComponent; }); } + +// Look up the component class in the glimmer component manager and return a +// function to construct components as if they were functions. +function glimmerComponentInstantiator(owner, componentKey) { + return args => { + const componentManager = owner.lookup('component-manager:glimmer'); + const componentClass = owner.factoryFor(`component:${componentKey}`).class; + return componentManager.createComponent(componentClass, { named: args }); + }; +} From f3300bcbf0574e1f0e299dc96b2e91657807e7e0 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 10:38:00 -0700 Subject: [PATCH 49/52] Remove the scenarios I didn't need/didn't get to --- ui/mirage/scenarios/topo.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/mirage/scenarios/topo.js b/ui/mirage/scenarios/topo.js index ed85f0f70..d38d28df5 100644 --- a/ui/mirage/scenarios/topo.js +++ b/ui/mirage/scenarios/topo.js @@ -45,8 +45,6 @@ export function topoSmall(server) { }); } -export function topoSmallProblems(server) {} - export function topoMedium(server) { server.createList('agent', 3); server.createList('node', 10, { @@ -109,7 +107,3 @@ export function topoMedium(server) { forceRunningClientStatus: true, }); } - -export function topoMediumBatch(server) {} - -export function topoMediumVariadic(server) {} From 2309baf0806783c141059029a7f39bf3f1097efe Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 10:40:32 -0700 Subject: [PATCH 50/52] Factor out the common sum aggregator used in the topology controller --- ui/app/controllers/topology.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index 5ca950f62..b32ce4bca 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -3,6 +3,8 @@ import { computed, action } from '@ember/object'; import classic from 'ember-classic-decorator'; import { reduceToLargestUnit } from 'nomad-ui/helpers/format-bytes'; +const sumAggregator = (sum, value) => sum + (value || 0); + @classic export default class TopologyControllers extends Controller { @computed('model.nodes.@each.datacenter') @@ -17,9 +19,7 @@ export default class TopologyControllers extends Controller { @computed('model.nodes.@each.resources') get totalMemory() { - const mibs = this.model.nodes - .mapBy('resources.memory') - .reduce((sum, memory) => sum + (memory || 0), 0); + const mibs = this.model.nodes.mapBy('resources.memory').reduce(sumAggregator, 0); return mibs * 1024 * 1024; } @@ -40,17 +40,13 @@ export default class TopologyControllers extends Controller { @computed('model.allocations.@each.allocatedResources') get totalReservedMemory() { - const mibs = this.model.allocations - .mapBy('allocatedResources.memory') - .reduce((sum, memory) => sum + (memory || 0), 0); + const mibs = this.model.allocations.mapBy('allocatedResources.memory').reduce(sumAggregator, 0); return mibs * 1024 * 1024; } @computed('model.allocations.@each.allocatedResources') get totalReservedCPU() { - return this.model.allocations - .mapBy('allocatedResources.cpu') - .reduce((sum, cpu) => sum + (cpu || 0), 0); + return this.model.allocations.mapBy('allocatedResources.cpu').reduce(sumAggregator, 0); } @computed('totalMemory', 'totalReservedMemory') @@ -80,12 +76,8 @@ export default class TopologyControllers extends Controller { get nodeUtilization() { const node = this.activeNode; const [formattedMemory, memoryUnits] = reduceToLargestUnit(node.memory * 1024 * 1024); - const totalReservedMemory = node.allocations - .mapBy('memory') - .reduce((sum, memory) => sum + (memory || 0), 0); - const totalReservedCPU = node.allocations - .mapBy('cpu') - .reduce((sum, cpu) => sum + (cpu || 0), 0); + const totalReservedMemory = node.allocations.mapBy('memory').reduce(sumAggregator, 0); + const totalReservedCPU = node.allocations.mapBy('cpu').reduce(sumAggregator, 0); return { totalMemoryFormatted: formattedMemory.toFixed(2), From 329fecac6d876aaecf71e166da1fa149ab0b4183 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 11:03:26 -0700 Subject: [PATCH 51/52] Work around Safari's lack of text transform support --- ui/app/styles/charts/topo-viz-node.scss | 7 +++++-- ui/app/templates/components/topo-viz/node.hbs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/app/styles/charts/topo-viz-node.scss b/ui/app/styles/charts/topo-viz-node.scss index cfef33320..1da8b4940 100644 --- a/ui/app/styles/charts/topo-viz-node.scss +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -64,8 +64,11 @@ .empty-text { fill: $red; transform: translate(50%, 50%); - text-anchor: middle; - alignment-baseline: central; + + text { + text-anchor: middle; + alignment-baseline: central; + } } & + .topo-viz-node { diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index fc02614a2..093eaaa1a 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -109,7 +109,7 @@ {{else}} - Empty Client + Empty Client {{/if}}
From 5c34e94529c6c5daa0afc6801f660a6eb414fd62 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 15 Oct 2020 11:04:08 -0700 Subject: [PATCH 52/52] Typo --- ui/app/components/flex-masonry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/flex-masonry.js b/ui/app/components/flex-masonry.js index 14760e6f7..5d29c54fa 100644 --- a/ui/app/components/flex-masonry.js +++ b/ui/app/components/flex-masonry.js @@ -15,7 +15,7 @@ export default class FlexMasonry extends Component { @action reflow() { run.next(() => { - // There's nothing to do if this is single column layout + // There's nothing to do if this is a single column layout if (!this.element || this.args.columns === 1 || !this.args.columns) return; const columns = new Array(this.args.columns).fill(null).map(() => ({