From 35c4fd03abb71cfb4833cf1b98907419b61fdd8e Mon Sep 17 00:00:00 2001 From: John Cowen Date: Thu, 16 Jan 2020 16:31:09 +0000 Subject: [PATCH] ui: Discovery Chain Improvements (#7036) * Reorganize resolvers so its clearer what's happening * Use entire route definition for id * Clean up a tiny bit more, use guid for ids instead of JSON * ui: Externalize disco-chain utils and add initial unit testing * Add some click outside-ness for de-highlighting things --- ui-v2/app/components/discovery-chain.js | 175 +++--------------- .../templates/components/resolver-card.hbs | 17 +- ui-v2/app/templates/components/route-card.hbs | 2 +- .../utils/components/discovery-chain/index.js | 105 +++++++++++ .../get-alternate-services-test.js | 53 ++++++ .../discovery-chain/get-resolvers-test.js | 95 ++++++++++ ui-v2/yarn.lock | 120 ++++++++---- 7 files changed, 376 insertions(+), 191 deletions(-) create mode 100644 ui-v2/app/utils/components/discovery-chain/index.js create mode 100644 ui-v2/tests/unit/utils/components/discovery-chain/get-alternate-services-test.js create mode 100644 ui-v2/tests/unit/utils/components/discovery-chain/get-resolvers-test.js diff --git a/ui-v2/app/components/discovery-chain.js b/ui-v2/app/components/discovery-chain.js index 45380c2a2..3ee5a2dab 100644 --- a/ui-v2/app/components/discovery-chain.js +++ b/ui-v2/app/components/discovery-chain.js @@ -3,93 +3,12 @@ import { inject as service } from '@ember/service'; import { set, get, computed } from '@ember/object'; import { next } from '@ember/runloop'; -const getNodesByType = function(nodes = {}, type) { - return Object.values(nodes).filter(item => item.Type === type); -}; - -const targetsToFailover = function(targets, a) { - let type; - const Targets = targets.map(function(b) { - // TODO: this isn't going to work past namespace for services - // with dots in the name - const [aRev, bRev] = [a, b].map(item => item.split('.').reverse()); - const types = ['Datacenter', 'Namespace', 'Service', 'Subset']; - return bRev.find(function(item, i) { - const res = item !== aRev[i]; - if (res) { - type = types[i]; - } - return res; - }); - }); - return { - Type: type, - Targets: Targets, - }; -}; -const getNodeResolvers = function(nodes = {}) { - const failovers = getFailovers(nodes); - const resolvers = {}; - Object.keys(nodes).forEach(function(key) { - const node = nodes[key]; - if (node.Type === 'resolver' && !failovers.includes(key.split(':').pop())) { - resolvers[node.Name] = node; - } - }); - return resolvers; -}; - -const getTargetResolvers = function(dc, nspace = 'default', targets = [], nodes = {}) { - const resolvers = {}; - Object.values(targets).forEach(item => { - let node = nodes[item.ID]; - if (node) { - if (typeof resolvers[item.Service] === 'undefined') { - resolvers[item.Service] = { - ID: item.ID, - Name: item.Service, - Children: [], - Failover: null, - Redirect: null, - }; - } - const resolver = resolvers[item.Service]; - let failoverable = resolver; - if (item.ServiceSubset) { - failoverable = item; - // TODO: Sometimes we have set the resolvers ID to the ID of the - // subset this just shifts the subset of the front of the URL for the moment - const temp = item.ID.split('.'); - temp.shift(); - resolver.ID = temp.join('.'); - resolver.Children.push(item); - } - if (typeof node.Resolver.Failover !== 'undefined') { - // TODO: Figure out if we can get rid of this - /* eslint ember/no-side-effects: "warn" */ - set(failoverable, 'Failover', targetsToFailover(node.Resolver.Failover.Targets, item.ID)); - } else { - const res = targetsToFailover([node.Resolver.Target], `service.${nspace}.${dc}`); - if (res.Type === 'Datacenter' || res.Type === 'Namespace') { - resolver.Children.push(item); - set(failoverable, 'Redirect', true); - } - } - } - }); - return Object.values(resolvers); -}; -const getFailovers = function(nodes = {}) { - const failovers = []; - Object.values(nodes) - .filter(item => item.Type === 'resolver') - .forEach(function(item) { - (get(item, 'Resolver.Failover.Targets') || []).forEach(failover => { - failovers.push(failover); - }); - }); - return failovers; -}; +import { + createRoute, + getSplitters, + getRoutes, + getResolvers, +} from 'consul-ui/utils/components/discovery-chain/index'; export default Component.extend({ dom: service('dom'), @@ -134,74 +53,33 @@ export default Component.extend({ this.ticker.destroy(this); }, splitters: computed('chain.Nodes', function() { - return getNodesByType(get(this, 'chain.Nodes'), 'splitter').map(function(item) { - set(item, 'ID', `splitter:${item.Name}`); - return item; - }); + return getSplitters(get(this, 'chain.Nodes')); }), - routers: computed('chain.Nodes', function() { - // Right now there should only ever be one 'Router'. - return getNodesByType(get(this, 'chain.Nodes'), 'router'); + routes: computed('chain.Nodes', function() { + return getRoutes(get(this, 'chain.Nodes'), this.dom.guid); }), - routes: computed('chain', 'routers', function() { - const routes = get(this, 'routers').reduce(function(prev, item) { - return prev.concat( - item.Routes.map(function(route, i) { - return { - ...route, - ID: `route:${item.Name}-${JSON.stringify(route.Definition.Match.HTTP)}`, - }; - }) - ); - }, []); - if (routes.length === 0) { - let nextNode = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Datacenter}`; - const splitterID = `splitter:${this.chain.ServiceName}`; - if (typeof this.chain.Nodes[splitterID] !== 'undefined') { - nextNode = splitterID; - } - routes.push({ - Default: true, - ID: `route:${this.chain.ServiceName}`, - Name: this.chain.ServiceName, - Definition: { - Match: { - HTTP: { - PathPrefix: '/', - }, - }, - }, - NextNode: nextNode, - }); - } - return routes; - }), - nodeResolvers: computed('chain.Nodes', function() { - return getNodeResolvers(get(this, 'chain.Nodes')); - }), - resolvers: computed('nodeResolvers.[]', 'chain.Targets', function() { - return getTargetResolvers( + resolvers: computed('chain.{Nodes,Targets}', function() { + return getResolvers( this.chain.Datacenter, this.chain.Namespace, get(this, 'chain.Targets'), - this.nodeResolvers + get(this, 'chain.Nodes') ); }), graph: computed('chain.Nodes', function() { const graph = this.dataStructs.graph(); - Object.entries(get(this, 'chain.Nodes')).forEach(function([key, item]) { + const router = this.chain.ServiceName; + Object.entries(get(this, 'chain.Nodes')).forEach(([key, item]) => { switch (item.Type) { case 'splitter': - item.Splits.forEach(function(splitter) { + item.Splits.forEach(splitter => { graph.addLink(`splitter:${item.Name}`, splitter.NextNode); }); break; case 'router': - item.Routes.forEach(function(route, i) { - graph.addLink( - `route:${item.Name}-${JSON.stringify(route.Definition.Match.HTTP)}`, - route.NextNode - ); + item.Routes.forEach((route, i) => { + route = createRoute(route, router, this.dom.guid); + graph.addLink(route.ID, route.NextNode); }); break; } @@ -212,18 +90,15 @@ export default Component.extend({ if (this.selectedId === '' || !this.dom.element(`#${this.selectedId}`)) { return {}; } - const getTypeFromId = function(id) { - return id.split(':').shift(); - }; const id = this.selectedId; - const type = getTypeFromId(id); + const type = id.split(':').shift(); const nodes = [id]; const edges = []; this.graph.forEachLinkedNode(id, (linkedNode, link) => { nodes.push(linkedNode.id); edges.push(`${link.fromId}>${link.toId}`); this.graph.forEachLinkedNode(linkedNode.id, (linkedNode, link) => { - const nodeType = getTypeFromId(linkedNode.id); + const nodeType = linkedNode.id.split(':').shift(); if (type !== nodeType && type !== 'splitter' && nodeType !== 'splitter') { nodes.push(linkedNode.id); edges.push(`${link.fromId}>${link.toId}`); @@ -248,6 +123,16 @@ export default Component.extend({ // TODO: Figure out if we can remove this next next(() => { this._listeners.remove(); + this._listeners.add(this.dom.document(), { + click: e => { + // all route/splitter/resolver components currently + // have classes that end in '-card' + if (!this.dom.closest('[class$="-card"]', e.target)) { + set(this, 'active', false); + set(this, 'selectedId', ''); + } + }, + }); [...this.dom.elements('path.split', this.element)].forEach(item => { this._listeners.add(item, { mouseover: e => this.actions.showSplit.apply(this, [e]), diff --git a/ui-v2/app/templates/components/resolver-card.hbs b/ui-v2/app/templates/components/resolver-card.hbs index b252c5bb6..c35821402 100644 --- a/ui-v2/app/templates/components/resolver-card.hbs +++ b/ui-v2/app/templates/components/resolver-card.hbs @@ -15,17 +15,6 @@ -{{else if item.Redirect}} -
-
Redirect
-
-
    -
  1. - {{item.ID}} -
  2. -
-
-
{{/if}} @@ -47,15 +36,15 @@ - {{else if item.Redirect}} + {{else if child.Redirect}}
Redirect
- {{child.ID}} + {{child.Name}}
{{else}} - {{child.ServiceSubset}} + {{child.Name}} {{/if}} diff --git a/ui-v2/app/templates/components/route-card.hbs b/ui-v2/app/templates/components/route-card.hbs index 5bb44fa0c..9d43619d4 100644 --- a/ui-v2/app/templates/components/route-card.hbs +++ b/ui-v2/app/templates/components/route-card.hbs @@ -9,7 +9,7 @@ {{/if}}
- {{path.type}} + {{path.type}}
{{#if (not-eq path.type 'Default')}} diff --git a/ui-v2/app/utils/components/discovery-chain/index.js b/ui-v2/app/utils/components/discovery-chain/index.js new file mode 100644 index 000000000..9f3534217 --- /dev/null +++ b/ui-v2/app/utils/components/discovery-chain/index.js @@ -0,0 +1,105 @@ +const getNodesByType = function(nodes = {}, type) { + return Object.values(nodes).filter(item => item.Type === type); +}; +const findResolver = function(resolvers, service, nspace = 'default', dc) { + if (typeof resolvers[service] === 'undefined') { + resolvers[service] = { + ID: `${service}.${nspace}.${dc}`, + Name: service, + Children: [], + }; + } + return resolvers[service]; +}; +export const getAlternateServices = function(targets, a) { + let type; + const Targets = targets.map(function(b) { + // TODO: this isn't going to work past namespace for services + // with dots in the name, but by the time that becomes an issue + // we might have more data from the endpoint so we don't have to guess + // right now the backend also doesn't support dots in service names + const [aRev, bRev] = [a, b].map(item => item.split('.').reverse()); + const types = ['Datacenter', 'Namespace', 'Service', 'Subset']; + return bRev.find(function(item, i) { + const res = item !== aRev[i]; + if (res) { + type = types[i]; + } + return res; + }); + }); + return { + Type: type, + Targets: Targets, + }; +}; + +export const getSplitters = function(nodes) { + return getNodesByType(nodes, 'splitter').map(function(item) { + // Splitters need IDs adding so we can find them in the DOM later + item.ID = `splitter:${item.Name}`; + return item; + }); +}; +export const getRoutes = function(nodes, uid) { + return getNodesByType(nodes, 'router').reduce(function(prev, item) { + return prev.concat( + item.Routes.map(function(route, i) { + // Routes also have IDs added via createRoute + return createRoute(route, item.Name, uid); + }) + ); + }, []); +}; +export const getResolvers = function(dc, nspace = 'default', targets = {}, nodes = {}) { + const resolvers = {}; + Object.values(targets).forEach(target => { + const node = nodes[`resolver:${target.ID}`]; + const resolver = findResolver(resolvers, target.Service, nspace, dc); + // We use this to figure out whether this target is a redirect target + const alternate = getAlternateServices([target.ID], `service.${nspace}.${dc}`); + + let failovers; + // Figure out the failover type + if (typeof node.Resolver.Failover !== 'undefined') { + failovers = getAlternateServices(node.Resolver.Failover.Targets, target.ID); + } + switch (true) { + // This target is a redirect + case alternate.Type !== 'Service': + resolver.Children.push({ + Redirect: true, + ID: target.ID, + Name: target[alternate.Type], + }); + break; + // This target is a Subset + case typeof target.ServiceSubset !== 'undefined': + resolver.Children.push({ + Subset: true, + ID: target.ID, + Name: target.ServiceSubset, + Filter: target.Subset.Filter, + ...(typeof failovers !== 'undefined' + ? { + Failover: failovers, + } + : {}), + }); + break; + // This target is just normal service that might have failovers + default: + if (typeof failovers !== 'undefined') { + resolver.Failover = failovers; + } + } + }); + return Object.values(resolvers); +}; +export const createRoute = function(route, router, uid) { + return { + ...route, + Default: typeof route.Definition.Match === 'undefined', + ID: `route:${router}-${uid(route.Definition)}`, + }; +}; diff --git a/ui-v2/tests/unit/utils/components/discovery-chain/get-alternate-services-test.js b/ui-v2/tests/unit/utils/components/discovery-chain/get-alternate-services-test.js new file mode 100644 index 000000000..a641d722b --- /dev/null +++ b/ui-v2/tests/unit/utils/components/discovery-chain/get-alternate-services-test.js @@ -0,0 +1,53 @@ +import { getAlternateServices } from 'consul-ui/utils/components/discovery-chain/index'; +import { module, test } from 'qunit'; + +module('Unit | Utility | components/discovery-chain/get-alternative-services', function() { + test('it guesses a different namespace', function(assert) { + const expected = { + Type: 'Namespace', + Targets: ['different-ns', 'different-ns2'], + }; + const actual = getAlternateServices( + ['service.different-ns.dc', 'service.different-ns2.dc'], + 'service.namespace.dc' + ); + assert.equal(actual.Type, expected.Type); + assert.deepEqual(actual.Targets, expected.Targets); + }); + test('it guesses a different datacenter', function(assert) { + const expected = { + Type: 'Datacenter', + Targets: ['dc1', 'dc2'], + }; + const actual = getAlternateServices( + ['service.namespace.dc1', 'service.namespace.dc2'], + 'service.namespace.dc' + ); + assert.equal(actual.Type, expected.Type); + assert.deepEqual(actual.Targets, expected.Targets); + }); + test('it guesses a different service', function(assert) { + const expected = { + Type: 'Service', + Targets: ['service-2', 'service-3'], + }; + const actual = getAlternateServices( + ['service-2.namespace.dc', 'service-3.namespace.dc'], + 'service.namespace.dc' + ); + assert.equal(actual.Type, expected.Type); + assert.deepEqual(actual.Targets, expected.Targets); + }); + test('it guesses a different subset', function(assert) { + const expected = { + Type: 'Subset', + Targets: ['v3', 'v2'], + }; + const actual = getAlternateServices( + ['v3.service.namespace.dc', 'v2.service.namespace.dc'], + 'v1.service.namespace.dc' + ); + assert.equal(actual.Type, expected.Type); + assert.deepEqual(actual.Targets, expected.Targets); + }); +}); diff --git a/ui-v2/tests/unit/utils/components/discovery-chain/get-resolvers-test.js b/ui-v2/tests/unit/utils/components/discovery-chain/get-resolvers-test.js new file mode 100644 index 000000000..b6e3706dd --- /dev/null +++ b/ui-v2/tests/unit/utils/components/discovery-chain/get-resolvers-test.js @@ -0,0 +1,95 @@ +import { getResolvers } from 'consul-ui/utils/components/discovery-chain/index'; +import { module, test } from 'qunit'; +import { get } from 'consul-ui/tests/helpers/api'; + +const dc = 'dc-1'; +const nspace = 'default'; +const request = { + url: `/v1/discovery-chain/service-name?dc=${dc}`, +}; +module('Unit | Utility | components/discovery-chain/get-resolvers', function() { + test('it assigns Subsets correctly', function(assert) { + return get(request.url, { + headers: { + cookie: { + CONSUL_RESOLVER_COUNT: 1, + CONSUL_SUBSET_COUNT: 1, + CONSUL_REDIRECT_COUNT: 0, + CONSUL_FAILOVER_COUNT: 0, + }, + }, + }).then(function({ Chain }) { + const actual = getResolvers(dc, nspace, Chain.Targets, Chain.Nodes); + const childId = Object.keys(Chain.Targets)[1]; + const target = Chain.Targets[`${childId}`]; + const firstChild = actual[0].Children[0]; + assert.equal(firstChild.Subset, true); + assert.equal(firstChild.ID, target.ID); + assert.equal(firstChild.Name, target.ServiceSubset); + }); + }); + test('it assigns Redirects correctly', function(assert) { + return get(request.url, { + headers: { + cookie: { + CONSUL_RESOLVER_COUNT: 1, + CONSUL_REDIRECT_COUNT: 1, + CONSUL_FAILOVER_COUNT: 0, + CONSUL_SUBSET_COUNT: 0, + }, + }, + }).then(function({ Chain }) { + const actual = getResolvers(dc, nspace, Chain.Targets, Chain.Nodes); + const childId = Object.keys(Chain.Targets)[1]; + const target = Chain.Targets[`${childId}`]; + const firstChild = actual[0].Children[0]; + assert.equal(firstChild.Redirect, true); + assert.equal(firstChild.ID, target.ID); + }); + }); + test('it assigns Failovers to Subsets correctly', function(assert) { + return Promise.all( + ['Datacenter', 'Namespace'].map(function(failoverType) { + return get(request.url, { + headers: { + cookie: { + CONSUL_RESOLVER_COUNT: 1, + CONSUL_REDIRECT_COUNT: 0, + CONSUL_SUBSET_COUNT: 1, + CONSUL_FAILOVER_COUNT: 1, + CONSUL_FAILOVER_TYPE: failoverType, + }, + }, + }).then(function({ Chain }) { + const actual = getResolvers(dc, nspace, Chain.Targets, Chain.Nodes); + const actualSubset = actual[0].Children[0]; + assert.equal(actualSubset.Subset, true); + assert.equal(actualSubset.Failover.Type, failoverType); + }); + }) + ); + }); + test('it assigns Failovers correctly', function(assert) { + return Promise.all( + ['Datacenter', 'Namespace'].map(function(failoverType, i) { + return get(request.url, { + headers: { + cookie: { + CONSUL_RESOLVER_COUNT: 1, + CONSUL_REDIRECT_COUNT: 0, + CONSUL_SUBSET_COUNT: 0, + CONSUL_FAILOVER_COUNT: 1, + CONSUL_FAILOVER_TYPE: failoverType, + }, + }, + }).then(function({ Chain }) { + const actual = getResolvers(dc, nspace, Chain.Targets, Chain.Nodes); + const node = Chain.Nodes[`resolver:${Object.keys(Chain.Targets)[0]}`]; + const expected = node.Resolver.Failover.Targets.map(item => item.split('.').reverse()[i]); + assert.equal(actual[0].Failover.Type, failoverType); + assert.deepEqual(actual[0].Failover.Targets, expected); + }); + }) + ); + }); +}); diff --git a/ui-v2/yarn.lock b/ui-v2/yarn.lock index b7e4fae78..c327ba63d 100644 --- a/ui-v2/yarn.lock +++ b/ui-v2/yarn.lock @@ -979,9 +979,9 @@ "@glimmer/util" "^0.42.0" "@hashicorp/api-double@^1.3.0": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.5.1.tgz#0c0d62f00622d87b4f9a99fcaa8f20cbb35d1796" - integrity sha512-24dsxjbjb5TtVlTdRiG9jFfRLIYqx8NkiyD2zjInzh/022n0dDMk5b1zFWTIcJulO4079Q2z08Cx3B1kDCe9mg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.6.0.tgz#23c48d1982b81b6c9164067354d7653320ba761f" + integrity sha512-U11NttTVvJUVOFH4bgS8eZ+0s6j4/4DYPz9xkJM2ciZBnG353l2G7LYeivt55QajnCl3ImevEO4vSlvvFf4I4Q== dependencies: array-range "^1.0.1" backtick-template "^0.2.0" @@ -992,9 +992,9 @@ js-yaml "^3.13.1" "@hashicorp/consul-api-double@^2.6.2": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.9.0.tgz#4489920942b2a7cc8276f15db6a38e414b34dd2f" - integrity sha512-a2GKU2pwLjmaC7OeCbEjHUjE9uYThe79OgxoHbT/8UrTO3yYssUDrlZke5hY8MObz03L+Qq6THRzonEYw4KpiQ== + version "2.11.0" + resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.11.0.tgz#0b833893ccc5cfb9546b1513127d5e92d30f2262" + integrity sha512-2MO1jiwuJyPlSGQ4AeFtLKJWmLSj0msoiaRHPtj6YPjm69ZkY/t4U4SU3cfpVn2Dx7wHzXe//9GvNHI1gRxAzg== "@hashicorp/ember-cli-api-double@^2.0.0": version "2.0.0" @@ -1634,9 +1634,9 @@ astral-regex@^1.0.0: integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== async-disk-cache@^1.2.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/async-disk-cache/-/async-disk-cache-1.3.4.tgz#a5c9f72f199a9933583659f57a0e11377884f816" - integrity sha512-qsIvGJ/XYZ5bSGf5vHt2aEQHZnyuehmk/+51rCJhpkZl4LtvOZ+STbhLbdFAJGYO+dLzUT5Bb4nLKqHBX83vhw== + version "1.3.5" + resolved "https://registry.yarnpkg.com/async-disk-cache/-/async-disk-cache-1.3.5.tgz#cc6206ed79bb6982b878fc52e0505e4f52b62a02" + integrity sha512-VZpqfR0R7CEOJZ/0FOTgWq70lCrZyS1rkI8PXugDUkTKyyAUgZ2zQ09gLhMkEn+wN8LYeUTPxZdXtlX/kmbXKQ== dependencies: debug "^2.1.3" heimdalljs "^0.2.3" @@ -2419,9 +2419,9 @@ binary-extensions@^1.0.0: integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== "binaryextensions@1 || 2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.2.tgz#c83c3d74233ba7674e4f313cb2a2b70f54e94b7c" - integrity sha512-xVNN69YGDghOqCCtA6FI7avYrr02mTJjOgB0/f1VPD3pJC8QEvjTKWc4epDx8AqxxA75NI0QpVM2gPJXUbE4Tg== + version "2.2.0" + resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.2.0.tgz#e7c6ba82d4f5f5758c26078fe8eea28881233311" + integrity sha512-bHhs98rj/7i/RZpCSJ3uk55pLXOItjIrh2sRQZSM6OoktScX+LxJzvlU+FELp9j3TdcddTmmYArLSGptCTwjuw== blank-object@^1.0.1: version "1.0.2" @@ -3339,11 +3339,16 @@ can-symlink@^1.0.0: dependencies: tmp "0.0.28" -caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000989: +caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30000989: version "1.0.30000989" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz#b9193e293ccf7e4426c5245134b8f2a56c0ac4b9" integrity sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw== +caniuse-lite@^1.0.30000844: + version "1.0.30001021" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001021.tgz#e75ed1ef6dbadd580ac7e7720bb16f07b083f254" + integrity sha512-wuMhT7/hwkgd8gldgp2jcrUjOU9RXJ4XxGumQeOsUr91l3WwmM68Cpa/ymCnWEDqakwFXhuDQbaKNHXBPgeE9g== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -3791,13 +3796,20 @@ continuable-cache@^0.3.1: resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f" integrity sha1-vXJ6f67XfnH/OYWskzUakSczrQ8= -convert-source-map@^1.1.0, convert-source-map@^1.5.1: +convert-source-map@^1.1.0: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== dependencies: safe-buffer "~5.1.1" +convert-source-map@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + cookie-parser@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.4.tgz#e6363de4ea98c3def9697b93421c09f30cf5d188" @@ -3856,7 +3868,12 @@ core-js@2.4.1: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" integrity sha1-TekR5mew6ukSTjQlS1OupvxhjT4= -core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.5: +core-js@^2.4.0, core-js@^2.5.0: + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== + +core-js@^2.6.5: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -4279,11 +4296,16 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.47: +electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.30: version "1.3.252" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.252.tgz#5b6261965b564a0f4df0f1c86246487897017f52" integrity sha512-NWJ5TztDnjExFISZHFwpoJjMbLUifsNBnx7u2JI0gCw6SbKyQYYWWtBHasO/jPtHym69F4EZuTpRNGN11MT/jg== +electron-to-chromium@^1.3.47: + version "1.3.335" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.335.tgz#5fb6084a25cb1e2542df91e62b62e1931a602303" + integrity sha512-ngKsDGd/xr2lAZvilxTfdvfEiQKmavyXd6irlswaHnewmXoz6JgbM9FUNwgp3NFIUHHegh1F87H8f5BJ8zABxw== + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -5684,10 +5706,10 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fake-xml-http-request@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.0.0.tgz#41a92f0ca539477700cb1dafd2df251d55dac8ff" - integrity sha512-UjNnynb6eLAB0lyh2PlTEkjRJORnNsVF1hbzU+PQv89/cyBV9GDRCy7JAcLQgeCLYT+3kaumWWZKEJvbaK74eQ== +fake-xml-http-request@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.0.1.tgz#e4a7f256af055d8059deb23c9d7ae721d28cf078" + integrity sha512-KzT+G4aLM1Btg25QRGxB6yGLGOVZXXzrH8I4OG3KHwsdoqFclyW3alieqh5NaYGcmbQvNOn/ldGO1rGKf7CNdA== faker@^4.1.0: version "4.1.0" @@ -6278,7 +6300,7 @@ glob@^5.0.10: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.1.2, glob@^7.1.4, glob@~7.1.1: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== @@ -6290,6 +6312,18 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, gl once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -8203,18 +8237,30 @@ mime-db@1.40.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + "mime-db@>= 1.40.0 < 2": version "1.41.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.41.0.tgz#9110408e1f6aa1b34aef51f2c9df3caddf46b6a0" integrity sha512-B5gxBI+2K431XW8C2rcc/lhppbuji67nf9v39eH8pkWoZDxnAL0PxdpH32KYRScniF8qDHBDlI+ipgg5WrCUYw== -mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.19, mime-types@~2.1.19, mime-types@~2.1.24: +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.19, mime-types@~2.1.19: version "2.1.24" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== dependencies: mime-db "1.40.0" +mime-types@~2.1.24: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -9288,12 +9334,12 @@ prepend-http@^2.0.0: integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= pretender@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pretender/-/pretender-2.1.1.tgz#5085f0a1272c31d5b57c488386f69e6ca207cb35" - integrity sha512-IkidsJzaroAanw3I43tKCFm2xCpurkQr9aPXv5/jpN+LfCwDaeI8rngVWtQZTx4qqbhc5zJspnLHJ4N/25KvDQ== + version "2.1.2" + resolved "https://registry.yarnpkg.com/pretender/-/pretender-2.1.2.tgz#02d7c0a3f18cb0ce376dfc4fb0043ca288f50316" + integrity sha512-5Jx7kBalWDn8oEKfw6nAcx2KK4GkDSQXG3WhgaPsDtak6Rv6nTeQjOdvOM9PEvauS+9Ur+DLfZTDWBtqK6lFVA== dependencies: "@xg-wang/whatwg-fetch" "^3.0.0" - fake-xml-http-request "^2.0.0" + fake-xml-http-request "~2.0.0" route-recognizer "^0.3.3" prettier@^1.10.2: @@ -9905,13 +9951,20 @@ resolve@1.9.0: dependencies: path-parse "^1.0.6" -resolve@^1.1.3, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: +resolve@^1.1.3, resolve@^1.1.7, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== dependencies: path-parse "^1.0.6" +resolve@^1.10.0, resolve@^1.3.3: + version "1.14.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" + integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== + dependencies: + path-parse "^1.0.6" + responselike@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -10810,7 +10863,12 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -symlink-or-copy@^1.0.0, symlink-or-copy@^1.0.1, symlink-or-copy@^1.1.8, symlink-or-copy@^1.2.0: +symlink-or-copy@^1.0.0, symlink-or-copy@^1.0.1, symlink-or-copy@^1.1.8: + version "1.3.1" + resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz#9506dd64d8e98fa21dcbf4018d1eab23e77f71fe" + integrity sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA== + +symlink-or-copy@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz#5d49108e2ab824a34069b68974486c290020b393" integrity sha512-W31+GLiBmU/ZR02Ii0mVZICuNEN9daZ63xZMPDsYgPgNjMtg+atqLEGI7PPI936jYSQZxoLb/63xos8Adrx4Eg== @@ -10968,9 +11026,9 @@ text-table@^0.2.0: integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= "textextensions@1 || 2": - version "2.5.0" - resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.5.0.tgz#e21d3831dafa37513dd80666dff541414e314293" - integrity sha512-1IkVr355eHcomgK7fgj1Xsokturx6L5S2JRT5WcRdA6v5shk9sxWuO/w/VbpQexwkXJMQIa/j1dBi3oo7+HhcA== + version "2.6.0" + resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4" + integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ== through2@^2.0.0: version "2.0.5"