diff --git a/ui/app/components/flex-masonry.js b/ui/app/components/flex-masonry.js new file mode 100644 index 00000000000..5d29c54faf2 --- /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 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(() => ({ + 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; + }); + + // 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) => { + 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/components/topo-viz.js b/ui/app/components/topo-viz.js new file mode 100644 index 00000000000..ee3616102d0 --- /dev/null +++ b/ui/app/components/topo-viz.js @@ -0,0 +1,271 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action, set } from '@ember/object'; +import { run } from '@ember/runloop'; +import { scaleLinear } from 'd3-scale'; +import { extent, deviation, mean } from 'd3-array'; +import { line, curveBasis } from 'd3-shape'; + +export default class TopoViz extends Component { + @tracked element = null; + @tracked topology = { datacenters: [] }; + + @tracked activeNode = null; + @tracked activeAllocation = null; + @tracked activeEdges = []; + @tracked edgeOffset = { x: 0, y: 0 }; + + 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.5; + if (variationCoefficient > threshold) return true; + return false; + } + + get datacenterIsSingleColumn() { + // If there are enough nodes, use two columns of nodes within + // a single column layout of datacenters to increase density. + 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, + datacenter: node.datacenter, + memory: node.resources.memory, + cpu: node.resources.cpu, + allocations: [], + isSelected: false, + }; + } + + dataForAllocation(allocation, node) { + const jobId = allocation.belongsTo('job').id(); + return { + allocation, + node, + jobId, + groupKey: JSON.stringify([jobId, allocation.taskGroupName]), + memory: allocation.allocatedResources.memory, + cpu: allocation.allocatedResources.cpu, + memoryPercent: allocation.allocatedResources.memory / node.memory, + cpuPercent: allocation.allocatedResources.cpu / node.cpu, + isSelected: false, + }; + } + + @action + buildTopology() { + const nodes = this.args.nodes; + const allocations = this.args.allocations; + + // 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; + }, {}); + + // 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; + } + + @action + captureElement(element) { + 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) { + 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 { + if (this.activeNode) { + set(this.activeNode, 'isSelected', false); + } + this.activeNode = null; + 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 && this.activeAllocation.allocation); + if (this.args.onNodeSelect) this.args.onNodeSelect(this.activeNode); + } + + @action + computedActiveEdges() { + // Wait a render cycle + run.next(() => { + 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()); + + // 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}"]`); + return [mem, cpu]; + }); + const selectedPoints = selectedPairs.map(pair => { + return pair.map(el => centerOfBBox(el.getBoundingClientRect())); + }); + + // 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/components/topo-viz/datacenter.js b/ui/app/components/topo-viz/datacenter.js new file mode 100644 index 00000000000..0750fc1eb08 --- /dev/null +++ b/ui/app/components/topo-viz/datacenter.js @@ -0,0 +1,32 @@ +import Component from '@glimmer/component'; + +export default class TopoVizDatacenter extends Component { + get scheduledAllocations() { + return this.args.datacenter.nodes.reduce( + (all, node) => all.concat(node.allocations.filterBy('allocation.isScheduled')), + [] + ); + } + + get aggregatedAllocationResources() { + return this.scheduledAllocations.reduce( + (totals, allocation) => { + totals.cpu += allocation.cpu; + totals.memory += allocation.memory; + return totals; + }, + { cpu: 0, memory: 0 } + ); + } + + get aggregatedNodeResources() { + return this.args.datacenter.nodes.reduce( + (totals, node) => { + totals.cpu += node.cpu; + totals.memory += node.memory; + return totals; + }, + { cpu: 0, memory: 0 } + ); + } +} diff --git a/ui/app/components/topo-viz/node.js b/ui/app/components/topo-viz/node.js new file mode 100644 index 00000000000..3bef48ee049 --- /dev/null +++ b/ui/app/components/topo-viz/node.js @@ -0,0 +1,182 @@ +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 dimensionsWidth = 0; + @tracked padding = 5; + @tracked activeAllocation = null; + + get height() { + return this.args.heightScale ? this.args.heightScale(this.args.node.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; + } + + get maskHeight() { + return this.height + this.yOffset; + } + + get totalHeight() { + return this.maskHeight + this.padding * 2; + } + + get maskId() { + return `topo-viz-node-mask-${guidFor(this)}`; + } + + get count() { + return this.args.node.allocations.length; + } + + get allocations() { + // 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('allocation.isScheduled').sort((a, b) => { + const deltaA = Math.abs(a.memoryPercent - a.cpuPercent); + const deltaB = Math.abs(b.memoryPercent - b.cpuPercent); + return deltaA - deltaB; + }); + } + + @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 - this.paddingLeft; + 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); + } + } + + @action + highlightAllocation(allocation) { + this.activeAllocation = allocation; + } + + @action + clearHighlight() { + 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); + } + + containsActiveTaskGroup() { + return this.args.node.allocations.some( + allocation => + allocation.taskGroupName === this.args.activeTaskGroup && + allocation.belongsTo('job').id() === this.args.activeJobId + ); + } + + computeData(width) { + const allocations = this.allocations; + let cpuOffset = 0; + let memoryOffset = 0; + + const cpu = []; + const memory = []; + 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; + if (isFirst) { + cpuWidth += 0.5; + memoryWidth += 0.5; + } + if (isSelected) { + cpuWidth--; + memoryWidth--; + } + + cpu.push({ + allocation, + offset: cpuOffset * 100, + percent: cpuPercent * 100, + width: cpuWidth, + x: cpuOffset * width + (isFirst ? 0 : 0.5) + (isSelected ? 0.5 : 0), + className: allocation.allocation.clientStatus, + }); + memory.push({ + allocation, + offset: memoryOffset * 100, + percent: memoryPercent * 100, + width: memoryWidth, + x: memoryOffset * width + (isFirst ? 0 : 0.5) + (isSelected ? 0.5 : 0), + className: allocation.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, + 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/controllers/topology.js b/ui/app/controllers/topology.js new file mode 100644 index 00000000000..b32ce4bcaa7 --- /dev/null +++ b/ui/app/controllers/topology.js @@ -0,0 +1,114 @@ +import Controller from '@ember/controller'; +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') + 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.mapBy('resources.memory').reduce(sumAggregator, 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.allocatedResources') + get totalReservedMemory() { + 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(sumAggregator, 0); + } + + @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; + } + + @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('activeNode') + get nodeUtilization() { + const node = this.activeNode; + const [formattedMemory, memoryUnits] = reduceToLargestUnit(node.memory * 1024 * 1024); + const totalReservedMemory = node.allocations.mapBy('memory').reduce(sumAggregator, 0); + const totalReservedCPU = node.allocations.mapBy('cpu').reduce(sumAggregator, 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(); + } + + @action + async setAllocation(allocation) { + if (allocation) { + await allocation.reload(); + await allocation.job.reload(); + } + this.set('activeAllocation', allocation); + } + + @action + setNode(node) { + this.set('activeNode', node); + } +} diff --git a/ui/app/helpers/format-bytes.js b/ui/app/helpers/format-bytes.js index b2c69ed065d..ba204fbfcbe 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/models/allocation.js b/ui/app/models/allocation.js index a9a60d351b0..3aa09fa1c7c 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/modifiers/window-resize.js b/ui/app/modifiers/window-resize.js new file mode 100644 index 00000000000..f70b20976d2 --- /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); + }; +}); diff --git a/ui/app/router.js b/ui/app/router.js index 12c3c3047dc..8a6cf648cd7 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/routes/topology.js b/ui/app/routes/topology.js new file mode 100644 index 00000000000..84875ceae29 --- /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({ + jobs: this.store.findAll('job'), + allocations: this.store.query('allocation', { resources: true }), + nodes: this.store.query('node', { resources: true }), + }).catch(notifyForbidden(this)); + } +} diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 15cccb6e377..1946bb98111 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 6a07ba0ac43..a689c40b267 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 cd642d64319..c82e009962d 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']; + arrayNullOverrides = ['Ports', 'Networks']; + + 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/app/styles/charts.scss b/ui/app/styles/charts.scss index bdb259dd269..3d494e09151 100644 --- a/ui/app/styles/charts.scss +++ b/ui/app/styles/charts.scss @@ -3,7 +3,9 @@ @import './charts/line-chart'; @import './charts/tooltip'; @import './charts/colors'; -@import './charts/chart-annotation.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/colors.scss b/ui/app/styles/charts/colors.scss index 6eedeeaecf4..dce0a3ddf5a 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 new file mode 100644 index 00000000000..1da8b49400d --- /dev/null +++ b/ui/app/styles/charts/topo-viz-node.scss @@ -0,0 +1,94 @@ +.topo-viz-node { + display: block; + + .label { + font-weight: $weight-normal; + } + + .chart { + display: inline-block; + height: 100%; + width: 100%; + overflow: visible; + + .node-background { + 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 { + fill: lighten($grey-lighter, 5%); + } + + .dimensions.is-active { + .bar { + opacity: 0.2; + + &.is-active { + opacity: 1; + } + } + } + + .bar { + cursor: pointer; + + &.is-selected { + stroke-width: 1px; + stroke: $blue; + fill: $blue-light; + } + } + + .label { + text-anchor: middle; + alignment-baseline: central; + font-weight: $weight-normal; + fill: $grey; + pointer-events: none; + } + } + + .empty-text { + fill: $red; + transform: translate(50%, 50%); + + text { + text-anchor: middle; + alignment-baseline: central; + } + } + + & + .topo-viz-node { + margin-top: 1em; + } + + &.is-empty { + .node-background { + stroke: $red; + stroke-width: 2; + fill: $white; + } + + .dimension-background { + fill: none; + } + } +} + +.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/styles/charts/topo-viz.scss b/ui/app/styles/charts/topo-viz.scss new file mode 100644 index 00000000000..bfbccb29dbb --- /dev/null +++ b/ui/app/styles/charts/topo-viz.scss @@ -0,0 +1,35 @@ +.topo-viz { + .topo-viz-datacenters { + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-content: space-between; + margin-top: -0.75em; + + .topo-viz-datacenter { + margin-top: 0.75em; + margin-bottom: 0.75em; + width: calc(50% - 0.75em); + } + } + + &.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.scss b/ui/app/styles/components.scss index 1f1058f3692..a032c11e6d5 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'; @@ -11,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'; @@ -20,6 +22,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/dashboard-metric.scss b/ui/app/styles/components/dashboard-metric.scss new file mode 100644 index 00000000000..5b04440a109 --- /dev/null +++ b/ui/app/styles/components/dashboard-metric.scss @@ -0,0 +1,50 @@ +.dashboard-metric { + &:not(:last-child) { + margin-bottom: 1.5em; + } + + &.column:not(:last-child) { + margin-bottom: 0; + } + + .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; + } + + &.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/components/flex-masonry.scss b/ui/app/styles/components/flex-masonry.scss new file mode 100644 index 00000000000..74385567235 --- /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/styles/components/legend.scss b/ui/app/styles/components/legend.scss new file mode 100644 index 00000000000..2e86faf97d1 --- /dev/null +++ b/ui/app/styles/components/legend.scss @@ -0,0 +1,33 @@ +.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; + } + + .legend-term { + display: inline-block; + whitespace: nowrap; + margin-right: 1.5em; + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/ui/app/styles/components/primary-metric.scss b/ui/app/styles/components/primary-metric.scss index e48b52d16e3..ba3777767b0 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/styles/core/columns.scss b/ui/app/styles/core/columns.scss index d912a37c31c..f4cdd5e62cc 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/styles/core/typography.scss b/ui/app/styles/core/typography.scss index 546320e6730..9294c5f1337 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/styles/core/variables.scss b/ui/app/styles/core/variables.scss index cee22b48fe4..24832cf1a19 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/flex-masonry.hbs b/ui/app/templates/components/flex-masonry.hbs new file mode 100644 index 00000000000..81777bf7bdc --- /dev/null +++ b/ui/app/templates/components/flex-masonry.hbs @@ -0,0 +1,13 @@ +
+ {{#each @items as |item|}} +
+ {{yield item (action this.reflow)}} +
+ {{/each}} +
diff --git a/ui/app/templates/components/gutter-menu.hbs b/ui/app/templates/components/gutter-menu.hbs index eb788f0b960..9d0608d3532 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/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs new file mode 100644 index 00000000000..63d123417d8 --- /dev/null +++ b/ui/app/templates/components/topo-viz.hbs @@ -0,0 +1,24 @@ +
+ + + + + {{#if this.activeAllocation}} + + + {{#each this.activeEdges as |edge|}} + + {{/each}} + + + {{/if}} +
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 00000000000..0a50a1f5a8d --- /dev/null +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -0,0 +1,19 @@ +
+
+ {{@datacenter.name}} + {{this.scheduledAllocations.length}} Allocs + {{@datacenter.nodes.length}} Nodes + {{this.aggregatedAllocationResources.memory}}/{{this.aggregatedNodeResources.memory}} MiB, + {{this.aggregatedAllocationResources.cpu}}/{{this.aggregatedNodeResources.cpu}} Mhz +
+
+ + + +
+
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 00000000000..093eaaa1a3c --- /dev/null +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -0,0 +1,116 @@ +
+ {{#unless @isDense}} +

+ {{#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 +

+ {{/unless}} + + + + + + + + {{#if this.allocations.length}} + + + {{#if this.data.memoryLabel}} + M + {{/if}} + {{#if this.data.memoryRemainder}} + + {{/if}} + {{#each this.data.memory key="allocation.id" as |memory|}} + + + {{#if (or (eq memory.className "starting") (eq memory.className "pending"))}} + + {{/if}} + + {{/each}} + + + {{#if this.data.cpuLabel}} + C + {{/if}} + {{#if this.data.cpuRemainder}} + + {{/if}} + {{#each this.data.cpu key="allocation.id" as |cpu|}} + + + {{#if (or (eq cpu.className "starting") (eq cpu.className "pending"))}} + + {{/if}} + + {{/each}} + + + {{else}} + Empty Client + {{/if}} + +
+ diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs new file mode 100644 index 00000000000..19c1fd515dc --- /dev/null +++ b/ui/app/templates/topology.hbs @@ -0,0 +1,204 @@ +{{title "Cluster Topology"}} + +
+
+
+
+
Legend
+
+
+

Metrics

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

Allocation Status

+
+
Running
+
Failed
+
Starting
+
+
+
+
+
+
+ {{#if this.activeNode}}Client{{else if this.activeAllocation}}Allocation{{else}}Cluster{{/if}} Details +
+
+ {{#if this.activeNode}} + {{#let this.activeNode.node as |node|}} +
+

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

+
+
+

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

+

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.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 +
+
+ {{/let}} + {{else if this.activeAllocation}} +
+

+ 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

+
+
+

{{this.scheduledAllocations.length}} Allocations

+
+
+
+

{{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/config/environment.js b/ui/config/environment.js index 9d758e0a62c..e459f26241f 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -25,8 +25,8 @@ module.exports = function(environment) { APP: { blockingQueries: true, - mirageScenario: 'smallCluster', - mirageWithNamespaces: true, + mirageScenario: 'topoMedium', + mirageWithNamespaces: false, mirageWithTokens: true, mirageWithRegions: true, }, diff --git a/ui/mirage/common.js b/ui/mirage/common.js index acfd6069d1d..cac53cdb0ac 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 c571271d039..d433340bc58 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,13 +194,13 @@ export default Factory.extend({ return server.create('task-resource', { allocation, name: task.name, - resources: task.Resources, + resources: task.originalResources, }); }); 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. diff --git a/ui/mirage/factories/node.js b/ui/mirage/factories/node.js index 03d6427d631..151536e1812 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-group.js b/ui/mirage/factories/task-group.js index 8e3cbc9ac8b..73ecf36f6f1 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/factories/task-resource.js b/ui/mirage/factories/task-resource.js index 782988bcda6..708cc761a31 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 6b6759c0795..ab3ee6359cd 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/scenarios/default.js b/ui/mirage/scenarios/default.js index fadd4f7343a..c30dc446ad6 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 00000000000..d38d28df523 --- /dev/null +++ b/ui/mirage/scenarios/topo.js @@ -0,0 +1,109 @@ +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', + nodeResources: genResources(3000, 5192), + }); + + 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: 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', 25, { + forceRunningClientStatus: true, + }); +} + +export function topoMedium(server) { + server.createList('agent', 3); + server.createList('node', 10, { + datacenter: 'us-west-1', + status: 'ready', + nodeResources: genResources(3000, 5192), + }); + server.createList('node', 12, { + datacenter: 'us-east-1', + status: 'ready', + nodeResources: genResources(3000, 5192), + }); + server.createList('node', 11, { + datacenter: 'eu-west-1', + status: 'ready', + nodeResources: genResources(3000, 5192), + }); + + server.createList('node', 8, { + datacenter: 'us-west-1', + status: 'ready', + nodeResources: genResources(8000, 12192), + }); + server.createList('node', 9, { + datacenter: 'us-east-1', + status: 'ready', + nodeResources: genResources(8000, 12192), + }); + + 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, + }); +} diff --git a/ui/mirage/serializers/allocation.js b/ui/mirage/serializers/allocation.js index eeaaf28f6f4..d2b58d4bb48 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/package.json b/ui/package.json index 35a3ca20a72..cd1b4b29215 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", @@ -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/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index d4788d88e1d..83fa2389809 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 9e95141f169..1ce46d80de4 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 0f3db7cccf0..13eb29b0410 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 a38508f203e..af4f240e1ff 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/topology-test.js b/ui/tests/acceptance/topology-test.js new file mode 100644 index 00000000000..79ca675a8c7 --- /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/acceptance/volume-detail-test.js b/ui/tests/acceptance/volume-detail-test.js index b951d54d077..c277dc4bba9 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 }); diff --git a/ui/tests/helpers/glimmer-factory.js b/ui/tests/helpers/glimmer-factory.js new file mode 100644 index 00000000000..c9cd865d318 --- /dev/null +++ b/ui/tests/helpers/glimmer-factory.js @@ -0,0 +1,32 @@ +// 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') +// +// 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; + }); +} + +// 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 }); + }; +} 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 00000000000..62bded80f83 --- /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); + } + }); + }); +}); 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 00000000000..5d4ae961f9f --- /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/integration/components/topo-viz/datacenter-test.js b/ui/tests/integration/components/topo-viz/datacenter-test.js new file mode 100644 index 00000000000..3480aae8814 --- /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/integration/components/topo-viz/node-test.js b/ui/tests/integration/components/topo-viz/node-test.js new file mode 100644 index 00000000000..14f1f903a0f --- /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.js b/ui/tests/pages/components/topo-viz.js new file mode 100644 index 00000000000..657e5a165d8 --- /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/pages/components/topo-viz/datacenter.js b/ui/tests/pages/components/topo-viz/datacenter.js new file mode 100644 index 00000000000..1388eeead53 --- /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 new file mode 100644 index 00000000000..665940ebf5a --- /dev/null +++ b/ui/tests/pages/components/topo-viz/node.js @@ -0,0 +1,36 @@ +import { attribute, collection, clickable, hasClass, isPresent, 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]'), + labelIsPresent: isPresent('[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]'), +}); diff --git a/ui/tests/pages/topology.js b/ui/tests/pages/topology.js new file mode 100644 index 00000000000..b9f2c63a6a6 --- /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]'), +}); 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 00000000000..ad4a018ef28 --- /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; + }, + }; + }, + }; +} diff --git a/ui/tests/unit/helpers/format-bytes-test.js b/ui/tests/unit/helpers/format-bytes-test.js index 14da4d13e34..b837d31be1a 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'); }); }); diff --git a/ui/yarn.lock b/ui/yarn.lock index 75b7a7316b0..4df1697a3fc 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" @@ -7471,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" @@ -8331,6 +9177,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 +9533,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 +9673,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 +9752,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 +9880,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 +9889,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 +10000,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 +13658,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"