From 7083c39b96dc00b6b874e661d128c04915a73809 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Wed, 26 May 2021 17:43:46 +0100 Subject: [PATCH] ui: Support Route optional parameters/segments (#10212) Moves our URLs with 'optional namespace segment' into a separately abstracted 'optional URL segment' feature --- .changelog/10212.txt | 3 + ui/.gitignore | 4 - ui/packages/consul-ui/app/abilities/nspace.js | 6 +- .../components/consul/upstream/list/index.hbs | 15 +- .../app/components/hashicorp-consul/index.hbs | 4 +- .../consul-ui/app/components/route/README.mdx | 1 + .../consul-ui/app/components/route/index.hbs | 3 +- .../consul-ui/app/components/route/index.js | 4 + .../topology-metrics/card/index.hbs | 2 +- .../consul-ui/app/controllers/application.js | 2 +- ui/packages/consul-ui/app/helpers/href-mut.js | 18 - ui/packages/consul-ui/app/helpers/href-to.js | 88 +++-- ui/packages/consul-ui/app/helpers/is-href.js | 1 + .../app/instance-initializers/href-to.js | 148 ++++++++ .../app/instance-initializers/nspace.js | 104 +----- .../app/locations/fsm-with-optional-test.js | 73 ++++ .../app/locations/fsm-with-optional.js | 317 ++++++++++++++++++ ui/packages/consul-ui/app/locations/fsm.js | 46 +++ .../app/mixins/token/with-actions.js | 2 +- ui/packages/consul-ui/app/router.js | 4 - ui/packages/consul-ui/app/routes/dc.js | 2 +- .../app/routes/dc/acls/auth-methods/index.js | 2 +- .../app/routes/dc/acls/auth-methods/show.js | 2 +- .../dc/acls/auth-methods/show/auth-method.js | 2 +- .../app/routes/dc/acls/policies/edit.js | 2 +- .../app/routes/dc/acls/policies/index.js | 2 +- .../app/routes/dc/acls/roles/edit.js | 2 +- .../app/routes/dc/acls/roles/index.js | 2 +- .../app/routes/dc/acls/tokens/index.js | 5 +- .../app/routes/dc/intentions/edit.js | 8 +- .../app/routes/dc/intentions/index.js | 2 +- .../consul-ui/app/routes/dc/kv/edit.js | 2 +- .../consul-ui/app/routes/dc/kv/index.js | 2 +- .../consul-ui/app/routes/dc/nodes/index.js | 2 +- .../consul-ui/app/routes/dc/nodes/show.js | 5 +- .../app/routes/dc/nodes/show/index.js | 1 + .../app/routes/dc/nodes/show/sessions.js | 4 +- .../consul-ui/app/routes/dc/services/index.js | 2 +- .../app/routes/dc/services/instance.js | 2 +- .../consul-ui/app/routes/dc/services/show.js | 2 +- .../app/routes/dc/services/show/index.js | 2 +- .../dc/services/show/intentions/index.js | 2 +- .../app/routes/dc/services/show/services.js | 2 +- ui/packages/consul-ui/app/routes/nspace.js | 73 ---- ui/packages/consul-ui/app/routing/route.js | 7 + ui/packages/consul-ui/app/routing/single.js | 2 +- ui/packages/consul-ui/app/services/env.js | 2 +- .../services/repository/nspace/disabled.js | 2 +- .../app/services/repository/nspace/enabled.js | 43 +-- ui/packages/consul-ui/app/services/routlet.js | 42 ++- .../app/utils/http/create-query-params.js | 2 +- .../app/utils/routing/transitionable.js | 6 +- ui/packages/consul-ui/config/environment.js | 5 +- ui/packages/consul-ui/package.json | 1 - .../dc/acls/auth-methods/index.feature | 96 +++--- .../dc/acls/tokens/own-no-delete.feature | 4 +- .../tests/helpers/get-nspace-runner.js | 16 +- .../integration/helpers/href-mut-test.js | 15 - .../tests/lib/page-object/visitable.js | 30 +- ui/packages/consul-ui/tests/steps.js | 11 +- .../tests/unit/routes/nspace-test.js | 11 - .../unit/utils/routing/transitionable-test.js | 19 -- ui/yarn.lock | 9 +- 63 files changed, 849 insertions(+), 451 deletions(-) create mode 100644 .changelog/10212.txt delete mode 100644 ui/packages/consul-ui/app/helpers/href-mut.js create mode 100644 ui/packages/consul-ui/app/instance-initializers/href-to.js create mode 100644 ui/packages/consul-ui/app/locations/fsm-with-optional-test.js create mode 100644 ui/packages/consul-ui/app/locations/fsm-with-optional.js create mode 100644 ui/packages/consul-ui/app/locations/fsm.js delete mode 100644 ui/packages/consul-ui/app/routes/nspace.js delete mode 100644 ui/packages/consul-ui/tests/integration/helpers/href-mut-test.js delete mode 100644 ui/packages/consul-ui/tests/unit/routes/nspace-test.js diff --git a/.changelog/10212.txt b/.changelog/10212.txt new file mode 100644 index 000000000..c4e2fb674 --- /dev/null +++ b/.changelog/10212.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Add 'optional route segments' and move namespaces to use them +``` diff --git a/ui/.gitignore b/ui/.gitignore index 98f2e8b08..08df27ddb 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -21,10 +21,6 @@ npm-debug.log* testem.log yarn-error.log -# storybook -storybook-static -**/.storybook/*.html - # ember-try .node_modules.ember-try bower.json.ember-try diff --git a/ui/packages/consul-ui/app/abilities/nspace.js b/ui/packages/consul-ui/app/abilities/nspace.js index 8d2e400d8..0dc1a233f 100644 --- a/ui/packages/consul-ui/app/abilities/nspace.js +++ b/ui/packages/consul-ui/app/abilities/nspace.js @@ -16,6 +16,10 @@ export default class NspaceAbility extends BaseAbility { } get canChoose() { - return this.env.var('CONSUL_NSPACES_ENABLED') && this.nspaces.length > 0; + return this.canUse && this.nspaces.length > 0; + } + + get canUse() { + return this.env.var('CONSUL_NSPACES_ENABLED'); } } diff --git a/ui/packages/consul-ui/app/components/consul/upstream/list/index.hbs b/ui/packages/consul-ui/app/components/consul/upstream/list/index.hbs index 8ed0ff618..8ce4b213e 100644 --- a/ui/packages/consul-ui/app/components/consul/upstream/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/upstream/list/index.hbs @@ -24,15 +24,22 @@ as |item index|> - {{#if (and (env 'CONSUL_NSPACES_ENABLED') (not-eq item.Namespace @nspace))}} - +{{#if (and (can 'use nspaces') (not-eq item.Namespace @nspace))}} + {{item.Name}} - {{else}} +{{else}} {{item.Name}} - {{/if}} +{{/if}} {{else}}

{{item.Name}} diff --git a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs index d69f37a1e..0adc50e1b 100644 --- a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs +++ b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs @@ -36,7 +36,7 @@ {{item.Name}} @@ -81,7 +81,7 @@ {{#each (reject-by 'DeletedAt' nspaces) as |item|}} {{item.Name}} diff --git a/ui/packages/consul-ui/app/components/route/README.mdx b/ui/packages/consul-ui/app/components/route/README.mdx index 2a75c7e99..b2b50bc7f 100644 --- a/ui/packages/consul-ui/app/components/route/README.mdx +++ b/ui/packages/consul-ui/app/components/route/README.mdx @@ -20,6 +20,7 @@ routes. | export | Type | Default | Description | | --- | --- | --- | --- | | `model` | `Object` | `undefined` | Arbitrary hash of data passed down from the parent route/outlet | +| `params` | `Object` | `undefined` | An object/merge of **all** optional route params and normal route params | ```hbs { + return item + .split('/') + .map(encodeURIComponent) + .join('/'); + }); } - return _hrefTo(owned, [targetRouteName, ...rest]); + return location.hrefTo(routeName, _params, _hash); + } catch (e) { + if (e.constructor === Error) { + e.message = `${e.message} For "${params[0]}:${JSON.stringify(params.slice(1))}"`; + } + throw e; } }; export default class HrefToHelper extends Helper { @service('router') router; - compute(params, hash) { - let href; - try { - href = hrefTo(this, this.router, params, hash); - } catch (e) { - e.message = `${e.message} For "${params[0]}:${JSON.stringify(params.slice(1))}"`; - throw e; - } - return href; + init() { + super.init(...arguments); + this.router.on('routeWillChange', this.routeWillChange); } - @observes('router.currentURL') - onURLChange() { + compute(params, hash) { + return hrefTo(getOwner(this), params, hash); + } + + @action + routeWillChange(transition) { this.recompute(); } + + willDestroy() { + this.router.off('routeWillChange', this.routeWillChange); + super.willDestroy(); + } } diff --git a/ui/packages/consul-ui/app/helpers/is-href.js b/ui/packages/consul-ui/app/helpers/is-href.js index 8d53fa2a5..1faf1a0dc 100644 --- a/ui/packages/consul-ui/app/helpers/is-href.js +++ b/ui/packages/consul-ui/app/helpers/is-href.js @@ -27,5 +27,6 @@ export default class IsHrefHelper extends Helper { willDestroy() { this.router.off('routeWillChange', this.routeWillChange); + super.willDestroy(); } } diff --git a/ui/packages/consul-ui/app/instance-initializers/href-to.js b/ui/packages/consul-ui/app/instance-initializers/href-to.js new file mode 100644 index 000000000..85f890e76 --- /dev/null +++ b/ui/packages/consul-ui/app/instance-initializers/href-to.js @@ -0,0 +1,148 @@ +import LinkComponent from '@ember/routing/link-component'; + +export class HrefTo { + constructor(container, target) { + this.applicationInstance = container; + this.target = target; + const hrefAttr = this.target.attributes.href; + this.url = hrefAttr && hrefAttr.value; + } + + handle(e) { + if (this.shouldHandle(e)) { + e.preventDefault(); + this.applicationInstance.lookup('router:main').location.transitionTo(this.url); + } + } + + shouldHandle(e) { + return ( + this.isUnmodifiedLeftClick(e) && + !this.isIgnored(this.target) && + !this.isExternal(this.target) && + !this.hasActionHelper(this.target) && + !this.hasDownload(this.target) && + !this.isLinkComponent(this.target) + ); + // && this.recognizeUrl(this.url); + } + + isUnmodifiedLeftClick(e) { + return (e.which === undefined || e.which === 1) && !e.ctrlKey && !e.metaKey; + } + + isExternal($el) { + return $el.getAttribute('target') === '_blank'; + } + + isIgnored($el) { + return $el.dataset.nativeHref; + } + + hasActionHelper($el) { + return $el.dataset.emberAction; + } + + hasDownload($el) { + return $el.hasAttribute('download'); + } + + isLinkComponent($el) { + let isLinkComponent = false; + const id = $el.id; + if (id) { + const componentInstance = this.applicationInstance.lookup('-view-registry:main')[id]; + isLinkComponent = componentInstance && componentInstance instanceof LinkComponent; + } + return isLinkComponent; + } + + recognizeUrl(url) { + let didRecognize = false; + + if (url) { + const router = this._getRouter(); + const rootUrl = this._getRootUrl(); + const isInternal = url.indexOf(rootUrl) === 0; + const urlWithoutRoot = this.getUrlWithoutRoot(); + const routerMicrolib = router._router._routerMicrolib || router._router.router; + + didRecognize = isInternal && routerMicrolib.recognizer.recognize(urlWithoutRoot); + } + + return didRecognize; + } + + getUrlWithoutRoot() { + const location = this.applicationInstance.lookup('router:main').location; + let url = location.getURL.apply( + { + getHash: () => '', + location: { + pathname: this.url, + }, + baseURL: location.baseURL, + rootURL: location.rootURL, + env: location.env, + }, + [] + ); + const pos = url.indexOf('?'); + if (pos !== -1) { + url = url.substr(0, pos - 1); + } + return url; + } + + _getRouter() { + return this.applicationInstance.lookup('service:router'); + } + + _getRootUrl() { + let router = this._getRouter(); + let rootURL = router.get('rootURL'); + + if (rootURL.charAt(rootURL.length - 1) !== '/') { + rootURL = rootURL + '/'; + } + + return rootURL; + } +} +function closestLink(el) { + if (el.closest) { + return el.closest('a'); + } else { + el = el.parentElement; + while (el && el.tagName !== 'A') { + el = el.parentElement; + } + return el; + } +} +export default { + name: 'href-to', + initialize(container) { + // we only want this to run in the browser, not in fastboot + if (typeof FastBoot === 'undefined') { + const dom = container.lookup('service:dom'); + const doc = dom.document(); + + const listener = e => { + const link = e.target.tagName === 'A' ? e.target : closestLink(e.target); + if (link) { + const hrefTo = new HrefTo(container, link); + hrefTo.handle(e); + } + }; + + doc.body.addEventListener('click', listener); + container.reopen({ + willDestroy() { + doc.body.removeEventListener('click', listener); + return this._super(...arguments); + }, + }); + } + }, +}; diff --git a/ui/packages/consul-ui/app/instance-initializers/nspace.js b/ui/packages/consul-ui/app/instance-initializers/nspace.js index 259665a4a..7611fe5c9 100644 --- a/ui/packages/consul-ui/app/instance-initializers/nspace.js +++ b/ui/packages/consul-ui/app/instance-initializers/nspace.js @@ -1,112 +1,12 @@ -import Route from '@ember/routing/route'; -import { routes } from 'consul-ui/router'; -import { env } from 'consul-ui/env'; -import flat from 'flat'; - -const withNspace = function(currentRouteName, requestedRouteName, ...rest) { - const isNspaced = currentRouteName.startsWith('nspace.'); - if (isNspaced && requestedRouteName.startsWith('dc')) { - return [`nspace.${requestedRouteName}`, ...rest]; - } - return [requestedRouteName, ...rest]; -}; - -const register = function(container, route, path) { - route.reopen({ - templateName: path - .replace('/root-create', '/create') - .replace('/create', '/edit') - .replace('/folder', '/index'), - }); - container.register(`route:nspace/${path}`, route); - const controller = container.resolveRegistration(`controller:${path}`); - if (controller) { - container.register(`controller:nspace/${path}`, controller); - } -}; - export function initialize(container) { - // patch Route routeName-like methods for navigation to support nspace relative routes - Route.reopen( - ['transitionTo', 'replaceWith'].reduce(function(prev, item) { - prev[item] = function(requestedRouteName, ...rest) { - return this._super(...withNspace(this.routeName, requestedRouteName, ...rest)); - }; - return prev; - }, {}) - ); - - // patch Route routeName-like methods for data to support nspace relative routes - Route.reopen( - ['modelFor', 'paramsFor'].reduce(function(prev, item) { - prev[item] = function(requestedRouteName, ...rest) { - const isNspaced = this.routeName.startsWith('nspace.'); - if (requestedRouteName === 'nspace' && !isNspaced && this.routeName !== 'nspace') { - return { - nspace: '~', - }; - } - return this._super(...withNspace(this.routeName, requestedRouteName, ...rest)); - }; - return prev; - }, {}) - ); - - // extend router service with a nspace aware router to support nspace relative routes - const nspacedRouter = container.resolveRegistration('service:router').extend({ - transitionTo: function(requestedRouteName, ...rest) { - return this._super(...withNspace(this.currentRoute.name, requestedRouteName, ...rest)); - }, - replaceWith: function(requestedRouteName, ...rest) { - return this._super(...withNspace(this.currentRoute.name, requestedRouteName, ...rest)); - }, - urlFor: function(requestedRouteName, ...rest) { - return this._super(...withNspace(this.currentRoute.name, requestedRouteName, ...rest)); - }, - }); - container.register('service:router', nspacedRouter); - - if (env('CONSUL_NSPACES_ENABLED')) { + const env = container.lookup('service:env'); + if (env.var('CONSUL_NSPACES_ENABLED')) { // enable the nspace repo ['dc', 'settings', 'dc.intentions.edit', 'dc.intentions.create'].forEach(function(item) { container.inject(`route:${item}`, 'nspacesRepo', 'service:repository/nspace/enabled'); container.inject(`route:nspace.${item}`, 'nspacesRepo', 'service:repository/nspace/enabled'); }); container.inject('route:application', 'nspacesRepo', 'service:repository/nspace/enabled'); - - const dotRe = /\./g; - // register automatic 'index' routes and controllers that start with 'dc' - Object.keys(flat(routes)) - .filter(function(item) { - return item.startsWith('dc'); - }) - .filter(function(item) { - return item.endsWith('path'); - }) - .map(function(item) { - return item.replace('._options.path', '').replace(dotRe, '/'); - }) - .forEach(function(item) { - let route = container.resolveRegistration(`route:${item}`); - let indexed; - // if the route doesn't exist it probably has an index route instead - if (!route) { - item = `${item}/index`; - route = container.resolveRegistration(`route:${item}`); - } else { - // if the route does exist - // then check to see if it also has an index route - indexed = `${item}/index`; - const index = container.resolveRegistration(`route:${indexed}`); - if (typeof index !== 'undefined') { - register(container, index, indexed); - } - } - - if (typeof route !== 'undefined') { - register(container, route, item); - } - }); } } diff --git a/ui/packages/consul-ui/app/locations/fsm-with-optional-test.js b/ui/packages/consul-ui/app/locations/fsm-with-optional-test.js new file mode 100644 index 000000000..6c7e698ec --- /dev/null +++ b/ui/packages/consul-ui/app/locations/fsm-with-optional-test.js @@ -0,0 +1,73 @@ +import FSMWithOptionalLocation from './fsm-with-optional'; +import { FSM, Location } from './fsm'; + +import { settled } from '@ember/test-helpers'; + +export default class FSMWithOptionalTestLocation extends FSMWithOptionalLocation { + implementation = 'fsm-with-optional-test'; + static create() { + return new this(...arguments); + } + constructor() { + super(...arguments); + this.location = new Location(); + this.machine = new FSM(this.location); + + // Browsers add event listeners to the state machine via the + // document/defaultView + this.doc = { + defaultView: { + addEventListener: (event, cb) => { + this.machine = new FSM(this.location, cb); + }, + removeEventListener: (event, cb) => { + this.machine = new FSM(); + }, + }, + }; + } + + visit(path) { + const app = this.container; + const router = this.container.lookup('router:main'); + + // taken from emberjs/application/instance:visit but cleaned up a little + // https://github.com/emberjs/ember.js/blob/21bd70c773dcc4bfe4883d7943e8a68d203b5bad/packages/%40ember/application/instance.js#L236-L277 + const handleTransitionResolve = async _ => { + await settled(); + return new Promise(resolve => setTimeout(resolve(app), 0)); + }; + const handleTransitionReject = error => { + if (error.error) { + throw error.error; + } else if (error.name === 'TransitionAborted' && router._routerMicrolib.activeTransition) { + return router._routerMicrolib.activeTransition.then( + handleTransitionResolve, + handleTransitionReject + ); + } else if (error.name === 'TransitionAborted') { + throw new Error(error.message); + } else { + throw error; + } + }; + // + + // the first time around, set up location via handleURL + if (this.location.pathname === '') { + // getting rootURL straight from env would be nicer but is non-standard + // and we still need access to router above + this.rootURL = router.rootURL.replace(/\/$/, ''); + // do some pre-setup setup so getURL can work + // this is machine setup that would be nice to via machine + // instantiation, its basically initialState + // move machine instantiation here once its an event target + this.machine.state.path = this.location.pathname = `${this.rootURL}${path}`; + this.path = this.getURL(); + // handleURL calls setupRouter for us + return app.handleURL(`${this.path}`).then(handleTransitionResolve, handleTransitionReject); + } + // anything else, just transitionTo like normal + return this.transitionTo(path).then(handleTransitionResolve, handleTransitionReject); + } +} diff --git a/ui/packages/consul-ui/app/locations/fsm-with-optional.js b/ui/packages/consul-ui/app/locations/fsm-with-optional.js new file mode 100644 index 000000000..3a307915c --- /dev/null +++ b/ui/packages/consul-ui/app/locations/fsm-with-optional.js @@ -0,0 +1,317 @@ +import { env } from 'consul-ui/env'; +const OPTIONAL = {}; +// if (true) { +// OPTIONAL.partition = /^-([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/; +// } +// +if (env('CONSUL_NSPACES_ENABLED')) { + OPTIONAL.nspace = /^~([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/; +} + +const trailingSlashRe = /\/$/; +const moreThan1SlashRe = /\/{2,}/g; + +const _uuid = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + return (c === 'x' ? r : (r & 3) | 8).toString(16); + }); +}; + +// let popstateFired = false; +/** + * Register a callback to be invoked whenever the browser history changes, + * including using forward and back buttons. + */ +const route = function(e) { + const path = e.state.path; + const url = this.getURLForTransition(path); + // Ignore initial page load popstate event in Chrome + // if (!popstateFired) { + // popstateFired = true; + // if (url === this._previousURL) { + // return; + // } + // } + if (url === this._previousURL) { + if (path === this._previousPath) { + return; + } + this._previousPath = e.state.path; + // async + this.container.lookup('route:application').refresh(); + } + if (typeof this.callback === 'function') { + // TODO: Can we use `settled` or similar to make this `route` method async? + // not async + this.callback(url); + } + // used for webkit workaround + this._previousURL = url; + this._previousPath = e.state.path; +}; +export default class FSMWithOptionalLocation { + // extend FSMLocation + implementation = 'fsm-with-optional'; + + baseURL = ''; + /** + * Set from router:main._setupLocation (-internals/routing/lib/system/router) + * Will be pre-pended to path upon state change + */ + rootURL = '/'; + + /** + * Path is the 'application path' i.e. the path/URL with no root/base URLs + * but potentially with optional parameters (these are remove when getURL is called) + */ + path = '/'; + + /** + * Sneaky undocumented property used in ember's main router used to skip any + * setup of location from the main router. We currently don't need this but + * document it here incase we ever do. + */ + cancelRouterSetup = false; + + /** + * Used to store our 'optional' segments should we have any + */ + optional = {}; + + static create() { + return new this(...arguments); + } + + constructor(owner, doc, env) { + this.container = Object.entries(owner)[0][1]; + + // add the route/state change handler + this.route = route.bind(this); + + this.doc = typeof doc === 'undefined' ? this.container.lookup('service:-document') : doc; + this.env = typeof env === 'undefined' ? this.container.lookup('service:env') : env; + + const base = this.doc.querySelector('base[href]'); + if (base !== null) { + this.baseURL = base.getAttribute('href'); + } + } + + /** + * @internal + * Called from router:main._setupLocation (-internals/routing/lib/system/router) + * Used to set state on first call to setURL + */ + initState() { + this.location = this.location || this.doc.defaultView.location; + this.machine = this.machine || this.doc.defaultView.history; + this.doc.defaultView.addEventListener('popstate', this.route); + + const state = this.machine.state; + const url = this.getURL(); + const href = this.formatURL(url); + + if (state && state.path === href) { + // preserve existing state + // used for webkit workaround, since there will be no initial popstate event + this._previousPath = href; + this._previousURL = url; + } else { + this.dispatch('replace', href); + } + } + + getURLFrom(url) { + // remove trailing slashes if they exists + url = url || this.location.pathname; + this.rootURL = this.rootURL.replace(trailingSlashRe, ''); + this.baseURL = this.baseURL.replace(trailingSlashRe, ''); + // remove baseURL and rootURL from start of path + return url + .replace(new RegExp(`^${this.baseURL}(?=/|$)`), '') + .replace(new RegExp(`^${this.rootURL}(?=/|$)`), '') + .replace(moreThan1SlashRe, '/'); // remove extra slashes + } + + getURLForTransition(url) { + this.optional = {}; + url = this.getURLFrom(url) + .split('/') + .filter((item, i) => { + if (i < 3) { + let found = false; + Object.entries(OPTIONAL).reduce((prev, [key, re]) => { + const res = re.exec(item); + if (res !== null) { + prev[key] = { + value: item, + match: res[1], + }; + found = true; + } + return prev; + }, this.optional); + return !found; + } + return true; + }) + .join('/'); + return url; + } + + optionalParams() { + let optional = this.optional || {}; + return Object.keys(OPTIONAL).reduce((prev, item) => { + let value = ''; + if (typeof optional[item] !== 'undefined') { + value = optional[item].match; + } + prev[item] = value; + return prev; + }, {}); + } + + // public entrypoints for app hrefs/URLs + + // visit and transitionTo can't be async/await as they return promise-like + // non-promises that get re-wrapped by the addition of async/await + visit() { + return this.transitionTo(...arguments); + } + + /** + * Turns a routeName into a full URL string for anchor hrefs etc. + */ + hrefTo(routeName, params, hash) { + if (typeof hash.dc !== 'undefined') { + delete hash.dc; + } + if (typeof hash.nspace !== 'undefined') { + hash.nspace = `~${hash.nspace}`; + } + // if (typeof hash.partition !== 'undefined') { + // hash.partition = `-${hash.partition}`; + // } + if (typeof this.router === 'undefined') { + this.router = this.container.lookup('router:main'); + } + const router = this.router._routerMicrolib; + const url = router.generate(routeName, ...params, { + queryParams: {}, + }); + let withOptional = true; + switch (true) { + case routeName === 'settings': + case routeName.startsWith('docs.'): + withOptional = false; + } + return this.formatURL(url, hash, withOptional); + } + + /** + * Takes a full browser URL including rootURL and optional (a full href) and + * performs an ember transition/refresh and browser location update using that + */ + transitionTo(url) { + const transitionURL = this.getURLForTransition(url); + if (this._previousURL === transitionURL) { + // probably an optional parameter change + this.dispatch('push', url); + return Promise.resolve(); + // this.setURL(url); + } else { + // use ember to transition, which will eventually come around to use location.setURL + return this.container.lookup('router:main').transitionTo(transitionURL); + } + } + + // + + // Ember location interface + + /** + * Returns the current `location.pathname` without `rootURL` or `baseURL` + */ + getURL() { + const search = this.location.search || ''; + let hash = ''; + if (typeof this.location.hash !== 'undefined') { + hash = this.location.hash.substr(0); + } + const url = this.getURLForTransition(this.location.pathname); + return `${url}${search}${hash}`; + } + + formatURL(url, optional, withOptional = true) { + if (url !== '') { + // remove trailing slashes if they exists + this.rootURL = this.rootURL.replace(trailingSlashRe, ''); + this.baseURL = this.baseURL.replace(trailingSlashRe, ''); + } else if (this.baseURL[0] === '/' && this.rootURL[0] === '/') { + // if baseURL and rootURL both start with a slash + // ... remove trailing slash from baseURL if it exists + this.baseURL = this.baseURL.replace(trailingSlashRe, ''); + } + + if (withOptional) { + const temp = url.split('/'); + if (Object.keys(optional || {}).length === 0) { + optional = undefined; + } + optional = Object.values(optional || this.optional || {}); + optional = optional.map(item => item.value || item, []); + temp.splice(...[1, 0].concat(optional)); + url = temp.join('/'); + } + + return `${this.baseURL}${this.rootURL}${url}`; + } + /** + * Change URL takes an ember application URL + */ + changeURL(type, path) { + this.path = path; + const state = this.machine.state; + path = this.formatURL(path); + + if (!state || state.path !== path) { + this.dispatch(type, path); + } + } + + setURL(path) { + // this.optional = {}; + this.changeURL('push', path); + } + + replaceURL(path) { + this.changeURL('replace', path); + } + + onUpdateURL(callback) { + this.callback = callback; + } + + // + + /** + * Dispatch takes a full actual browser URL with all the rootURL and optional + * params if they exist + */ + dispatch(event, path) { + const state = { + path: path, + uuid: _uuid(), + }; + this.machine[`${event}State`](state, null, path); + // popstate listeners only run from a browser action not when a state change + // is called directly, so manually call the popstate listener. + // https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#the_history_stack + this.route({ state: state }); + } + + willDestroy() { + this.doc.defaultView.removeEventListener('popstate', this.route); + } +} diff --git a/ui/packages/consul-ui/app/locations/fsm.js b/ui/packages/consul-ui/app/locations/fsm.js new file mode 100644 index 000000000..4f09e6980 --- /dev/null +++ b/ui/packages/consul-ui/app/locations/fsm.js @@ -0,0 +1,46 @@ +// a simple state machine that the History API happens to more or less implement +// it should really be an EventTarget but what we need here is simple enough +export class FSM { + // extends EventTarget/EventSource + state = {}; + constructor(location, listener = () => {}) { + this.listener = listener; + this.location = location; + } + /** + * @param state The infinite/extended state or context + * @param _ `_` was meant to be title but was never used, don't use this + * argument for anything unless browsers change, see: + * https://github.com/whatwg/html/issues/2174 + * @param path The state/event + */ + pushState(state, _, path) { + this.state = state; + this.location.pathname = path; + this.listener({ state: this.state }); + } + replaceState() { + return this.pushState(...arguments); + } +} + +export class Location { + pathname = ''; + search = ''; + hash = ''; +} + +export default class FSMLocation { + implementation = 'fsm'; + static create() { + return new this(...arguments); + } + constructor(owner) { + this.container = Object.entries(owner)[0][1]; + } + visit() { + return this.transitionTo(...arguments); + } + hrefTo() {} + transitionTo() {} +} diff --git a/ui/packages/consul-ui/app/mixins/token/with-actions.js b/ui/packages/consul-ui/app/mixins/token/with-actions.js index f3d1ad2a3..a2e972067 100644 --- a/ui/packages/consul-ui/app/mixins/token/with-actions.js +++ b/ui/packages/consul-ui/app/mixins/token/with-actions.js @@ -9,8 +9,8 @@ export default Mixin.create(WithBlockingActions, { use: function(item) { return this.repo .findBySlug({ - ns: this.modelFor('nspace').nspace.substr(1), dc: this.modelFor('dc').dc.Name, + ns: get(item, 'Namespace'), id: get(item, 'AccessorID'), }) .then(item => { diff --git a/ui/packages/consul-ui/app/router.js b/ui/packages/consul-ui/app/router.js index f17c65d20..349dca47f 100644 --- a/ui/packages/consul-ui/app/router.js +++ b/ui/packages/consul-ui/app/router.js @@ -235,10 +235,6 @@ if (env('CONSUL_NSPACES_ENABLED')) { }, }, }; - routes.nspace = { - _options: { path: '/:nspace' }, - dc: routes.dc, - }; } runInDebug(() => { // check to see if we are running docfy and if so add its routes to our diff --git a/ui/packages/consul-ui/app/routes/dc.js b/ui/packages/consul-ui/app/routes/dc.js index dd99ba0a2..ba26d76c2 100644 --- a/ui/packages/consul-ui/app/routes/dc.js +++ b/ui/packages/consul-ui/app/routes/dc.js @@ -33,7 +33,7 @@ export default class DcRoute extends Route { let [token, nspace, dc] = await Promise.all([ this.settingsRepo.findBySlug('token'), - this.nspacesRepo.getActive(), + this.nspacesRepo.getActive(this.optionalParams().nspace), this.repo.findBySlug(params.dc, app.dcs), ]); // if there is only 1 namespace then use that diff --git a/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/index.js b/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/index.js index 4702ec091..57a4b9e78 100644 --- a/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/index.js +++ b/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/index.js @@ -24,7 +24,7 @@ export default class IndexRoute extends Route { ...this.repo.status({ items: this.repo.findAllByDatacenter({ dc: this.modelFor('dc').dc.Name, - ns: this.modelFor('nspace').nspace.substr(1), + ns: this.optionalParams().nspace, }), }), searchProperties: this.queryParams.searchproperty.empty[0], diff --git a/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/show.js b/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/show.js index 6f543e1ae..6833057ab 100644 --- a/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/show.js +++ b/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/show.js @@ -8,7 +8,7 @@ export default class ShowRoute extends SingleRoute { model(params) { const dc = this.modelFor('dc').dc; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; return super.model(...arguments).then(model => { return hash({ diff --git a/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/show/auth-method.js b/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/show/auth-method.js index 61d0e66bf..75ea58d0d 100644 --- a/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/show/auth-method.js +++ b/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/show/auth-method.js @@ -1,7 +1,7 @@ import Route from 'consul-ui/routing/route'; export default class AuthMethodRoute extends Route { - model() { + model(params) { const parent = this.routeName .split('.') .slice(0, -1) diff --git a/ui/packages/consul-ui/app/routes/dc/acls/policies/edit.js b/ui/packages/consul-ui/app/routes/dc/acls/policies/edit.js index 145952393..201c400ef 100644 --- a/ui/packages/consul-ui/app/routes/dc/acls/policies/edit.js +++ b/ui/packages/consul-ui/app/routes/dc/acls/policies/edit.js @@ -14,7 +14,7 @@ export default class EditRoute extends SingleRoute.extend(WithPolicyActions) { model(params) { const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const tokenRepo = this.tokenRepo; return super.model(...arguments).then(model => { return hash({ diff --git a/ui/packages/consul-ui/app/routes/dc/acls/policies/index.js b/ui/packages/consul-ui/app/routes/dc/acls/policies/index.js index 68a3fba07..36dcb4e86 100644 --- a/ui/packages/consul-ui/app/routes/dc/acls/policies/index.js +++ b/ui/packages/consul-ui/app/routes/dc/acls/policies/index.js @@ -27,7 +27,7 @@ export default class IndexRoute extends Route.extend(WithPolicyActions) { return hash({ ...this.repo.status({ items: this.repo.findAllByDatacenter({ - ns: this.modelFor('nspace').nspace.substr(1), + ns: this.optionalParams().nspace, dc: this.modelFor('dc').dc.Name, }), }), diff --git a/ui/packages/consul-ui/app/routes/dc/acls/roles/edit.js b/ui/packages/consul-ui/app/routes/dc/acls/roles/edit.js index 524d5ce80..1ac185513 100644 --- a/ui/packages/consul-ui/app/routes/dc/acls/roles/edit.js +++ b/ui/packages/consul-ui/app/routes/dc/acls/roles/edit.js @@ -14,7 +14,7 @@ export default class EditRoute extends SingleRoute.extend(WithRoleActions) { model(params) { const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const tokenRepo = this.tokenRepo; return super.model(...arguments).then(model => { return hash({ diff --git a/ui/packages/consul-ui/app/routes/dc/acls/roles/index.js b/ui/packages/consul-ui/app/routes/dc/acls/roles/index.js index ab06f02d1..936c8835f 100644 --- a/ui/packages/consul-ui/app/routes/dc/acls/roles/index.js +++ b/ui/packages/consul-ui/app/routes/dc/acls/roles/index.js @@ -23,7 +23,7 @@ export default class IndexRoute extends Route.extend(WithRoleActions) { return hash({ ...this.repo.status({ items: this.repo.findAllByDatacenter({ - ns: this.modelFor('nspace').nspace.substr(1), + ns: this.optionalParams().nspace, dc: this.modelFor('dc').dc.Name, }), }), diff --git a/ui/packages/consul-ui/app/routes/dc/acls/tokens/index.js b/ui/packages/consul-ui/app/routes/dc/acls/tokens/index.js index 1441e1ea1..ed36ced26 100644 --- a/ui/packages/consul-ui/app/routes/dc/acls/tokens/index.js +++ b/ui/packages/consul-ui/app/routes/dc/acls/tokens/index.js @@ -33,14 +33,15 @@ export default class IndexRoute extends Route.extend(WithTokenActions) { } model(params) { + const nspace = this.optionalParams().nspace; return hash({ ...this.repo.status({ items: this.repo.findAllByDatacenter({ - ns: this.modelFor('nspace').nspace.substr(1), + ns: nspace, dc: this.modelFor('dc').dc.Name, }), }), - nspace: this.modelFor('nspace').nspace.substr(1), + nspace: nspace, token: this.settings.findBySlug('token'), searchProperties: this.queryParams.searchproperty.empty[0], }); diff --git a/ui/packages/consul-ui/app/routes/dc/intentions/edit.js b/ui/packages/consul-ui/app/routes/dc/intentions/edit.js index 52d150235..30f7f2f79 100644 --- a/ui/packages/consul-ui/app/routes/dc/intentions/edit.js +++ b/ui/packages/consul-ui/app/routes/dc/intentions/edit.js @@ -5,16 +5,16 @@ export default class EditRoute extends Route { @service('repository/intention') repo; @service('env') env; - async model({ intention_id }, transition) { + async model(params, transition) { const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; let item; - if (typeof intention_id !== 'undefined') { + if (typeof params.intention_id !== 'undefined') { item = await this.repo.findBySlug({ ns: nspace, dc: dc, - id: intention_id, + id: params.intention_id, }); } else { const defaultNspace = this.env.var('CONSUL_NSPACES_ENABLED') ? '*' : 'default'; diff --git a/ui/packages/consul-ui/app/routes/dc/intentions/index.js b/ui/packages/consul-ui/app/routes/dc/intentions/index.js index 4195ad9fb..0b2457fcc 100644 --- a/ui/packages/consul-ui/app/routes/dc/intentions/index.js +++ b/ui/packages/consul-ui/app/routes/dc/intentions/index.js @@ -17,7 +17,7 @@ export default class IndexRoute extends Route { async model(params) { return { dc: this.modelFor('dc').dc.Name, - nspace: this.modelFor('nspace').nspace.substr(1), + nspace: this.optionalParams().nspace, searchProperties: this.queryParams.searchproperty.empty[0], }; } diff --git a/ui/packages/consul-ui/app/routes/dc/kv/edit.js b/ui/packages/consul-ui/app/routes/dc/kv/edit.js index f18fca372..34f0a106d 100644 --- a/ui/packages/consul-ui/app/routes/dc/kv/edit.js +++ b/ui/packages/consul-ui/app/routes/dc/kv/edit.js @@ -18,7 +18,7 @@ export default class EditRoute extends Route { .indexOf('create') !== -1; const key = params.key; const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; return hash({ dc: dc, nspace: nspace || 'default', diff --git a/ui/packages/consul-ui/app/routes/dc/kv/index.js b/ui/packages/consul-ui/app/routes/dc/kv/index.js index 5b043c6a3..ebdbf6b14 100644 --- a/ui/packages/consul-ui/app/routes/dc/kv/index.js +++ b/ui/packages/consul-ui/app/routes/dc/kv/index.js @@ -29,7 +29,7 @@ export default class IndexRoute extends Route { model(params) { let key = params.key || '/'; const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; return hash({ parent: this.repo.findBySlug({ ns: nspace, diff --git a/ui/packages/consul-ui/app/routes/dc/nodes/index.js b/ui/packages/consul-ui/app/routes/dc/nodes/index.js index aa0b1d764..877aa5b79 100644 --- a/ui/packages/consul-ui/app/routes/dc/nodes/index.js +++ b/ui/packages/consul-ui/app/routes/dc/nodes/index.js @@ -19,7 +19,7 @@ export default class IndexRoute extends Route { async model(params) { const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const items = this.data.source(uri => uri`/${nspace}/${dc}/nodes`); const leader = this.data.source(uri => uri`/${nspace}/${dc}/leader`); return { diff --git a/ui/packages/consul-ui/app/routes/dc/nodes/show.js b/ui/packages/consul-ui/app/routes/dc/nodes/show.js index 087dc573f..4f2c9a1b7 100644 --- a/ui/packages/consul-ui/app/routes/dc/nodes/show.js +++ b/ui/packages/consul-ui/app/routes/dc/nodes/show.js @@ -3,12 +3,11 @@ import Route from 'consul-ui/routing/route'; import { hash } from 'rsvp'; export default class ShowRoute extends Route { - @service('data-source/service') - data; + @service('data-source/service') data; model(params) { const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const name = params.name; return hash({ dc: dc, diff --git a/ui/packages/consul-ui/app/routes/dc/nodes/show/index.js b/ui/packages/consul-ui/app/routes/dc/nodes/show/index.js index f08762f5b..36ad4bfe0 100644 --- a/ui/packages/consul-ui/app/routes/dc/nodes/show/index.js +++ b/ui/packages/consul-ui/app/routes/dc/nodes/show/index.js @@ -7,6 +7,7 @@ export default class IndexRoute extends Route { .split('.') .slice(0, -1) .join('.'); + model = this.modelFor(parent); // the default selected tab depends on whether you have any healthchecks or not // so check the length here. const to = get(model, 'item.Checks.length') > 0 ? 'healthchecks' : 'services'; diff --git a/ui/packages/consul-ui/app/routes/dc/nodes/show/sessions.js b/ui/packages/consul-ui/app/routes/dc/nodes/show/sessions.js index fb8bd9c7b..fd1708370 100644 --- a/ui/packages/consul-ui/app/routes/dc/nodes/show/sessions.js +++ b/ui/packages/consul-ui/app/routes/dc/nodes/show/sessions.js @@ -14,13 +14,13 @@ export default class SessionsRoute extends Route.extend(WithBlockingActions) { @service('feedback') feedback; - model() { + model(params) { const parent = this.routeName .split('.') .slice(0, -1) .join('.'); const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const node = this.paramsFor(parent).name; return hash({ dc: dc, diff --git a/ui/packages/consul-ui/app/routes/dc/services/index.js b/ui/packages/consul-ui/app/routes/dc/services/index.js index 873225c78..ba0a29b5e 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/index.js +++ b/ui/packages/consul-ui/app/routes/dc/services/index.js @@ -20,7 +20,7 @@ export default class IndexRoute extends Route { }; async model(params, transition) { - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const dc = this.modelFor('dc').dc.Name; const items = this.data.source(uri => uri`/${nspace}/${dc}/services`); return { diff --git a/ui/packages/consul-ui/app/routes/dc/services/instance.js b/ui/packages/consul-ui/app/routes/dc/services/instance.js index 16ceae8ae..b411359e4 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/instance.js +++ b/ui/packages/consul-ui/app/routes/dc/services/instance.js @@ -7,7 +7,7 @@ export default class InstanceRoute extends Route { async model(params, transition) { const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const item = await this.data.source( uri => uri`/${nspace}/${dc}/service-instance/${params.id}/${params.node}/${params.name}` diff --git a/ui/packages/consul-ui/app/routes/dc/services/show.js b/ui/packages/consul-ui/app/routes/dc/services/show.js index 7d87b0130..da35baafc 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show.js @@ -8,7 +8,7 @@ export default class ShowRoute extends Route { async model(params, transition) { const dc = this.modelFor('dc').dc; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const slug = params.name; let chain; diff --git a/ui/packages/consul-ui/app/routes/dc/services/show/index.js b/ui/packages/consul-ui/app/routes/dc/services/show/index.js index 983bfef6a..3e2bd9dea 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show/index.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show/index.js @@ -1,4 +1,4 @@ -import Route from '@ember/routing/route'; +import Route from 'consul-ui/routing/route'; import { get } from '@ember/object'; export default class IndexRoute extends Route { diff --git a/ui/packages/consul-ui/app/routes/dc/services/show/intentions/index.js b/ui/packages/consul-ui/app/routes/dc/services/show/intentions/index.js index 0468f0295..ab08f1739 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show/intentions/index.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show/intentions/index.js @@ -17,7 +17,7 @@ export default class IndexRoute extends Route { async model(params) { return { dc: this.modelFor('dc').dc.Name, - nspace: this.modelFor('nspace').nspace.substr(1) || 'default', + nspace: this.optionalParams().nspace || 'default', slug: this.paramsFor('dc.services.show').name, searchProperties: this.queryParams.searchproperty.empty[0], }; diff --git a/ui/packages/consul-ui/app/routes/dc/services/show/services.js b/ui/packages/consul-ui/app/routes/dc/services/show/services.js index dd2abff12..c30fad20d 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show/services.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show/services.js @@ -19,7 +19,7 @@ export default class ServicesRoute extends Route { async model(params, transition) { const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const parent = this.routeName .split('.') .slice(0, -1) diff --git a/ui/packages/consul-ui/app/routes/nspace.js b/ui/packages/consul-ui/app/routes/nspace.js deleted file mode 100644 index 148c28846..000000000 --- a/ui/packages/consul-ui/app/routes/nspace.js +++ /dev/null @@ -1,73 +0,0 @@ -import { inject as service } from '@ember/service'; -import Route from 'consul-ui/routing/route'; -import { hash } from 'rsvp'; -import { getOwner } from '@ember/application'; -import { env } from 'consul-ui/env'; -import transitionable from 'consul-ui/utils/routing/transitionable'; - -const DEFAULT_NSPACE_PARAM = '~default'; -export default class NspaceRoute extends Route { - @service('repository/dc') - repo; - - @service('router') - router; - - // The ember router seems to change the priority of individual routes - // depending on whether they are wildcard routes or not. - // This means that the namespace routes will be recognized before kv ones - // even though we define namespace routes after kv routes (kv routes are - // wildcard routes) - // Therefore here whenever we detect that ember has recognized a nspace route - // when it shouldn't (we know this as there is no ~ in the nspace param) - // we recalculate the route it should have caught by generating the nspace - // equivalent route for the url (/dc-1/kv/services > /~default/dc-1/kv/services) - // and getting the information for that route. We then remove the nspace specific - // information that we generated onto the route, which leaves us with the route - // we actually want. Using this final route information we redirect the user - // to where they wanted to go. - beforeModel(transition) { - if (!this.paramsFor('nspace').nspace.startsWith('~')) { - const url = `${env('rootURL')}${DEFAULT_NSPACE_PARAM}${transition.intent.url}`; - const route = this.router.recognize(url); - const [name, ...params] = transitionable(route, {}, getOwner(this)); - this.replaceWith.apply(this, [ - // remove the 'nspace.' from the routeName - name - .split('.') - .slice(1) - .join('.'), - // remove the nspace param from the params - ...params.slice(1), - ]); - } - } - - model(params) { - return hash({ - item: this.repo.getActive(), - nspace: params.nspace, - }); - } - - /** - * We need to redirect if someone doesn't specify the section they want, - * but not redirect if the 'section' is specified - * (i.e. /dc-1/ vs /dc-1/services). - * - * If the target route of the transition is `nspace.index`, it means that - * someone didn't specify a section and thus we forward them on to a - * default `.services` subroute. The specific services route we target - * depends on whether or not a namespace was specified. - * - */ - afterModel(model, transition) { - if (transition.to.name === 'nspace.index') { - if (model.nspace.startsWith('~')) { - this.transitionTo('nspace.dc.services', model.nspace, model.item.Name); - } else { - this.transitionTo('dc.services', model.nspace); - } - } - } -} diff --git a/ui/packages/consul-ui/app/routing/route.js b/ui/packages/consul-ui/app/routing/route.js index 3b9ea3d90..6bff28e1c 100644 --- a/ui/packages/consul-ui/app/routing/route.js +++ b/ui/packages/consul-ui/app/routing/route.js @@ -10,7 +10,10 @@ import wildcard from 'consul-ui/utils/routing/wildcard'; const isWildcard = wildcard(routes); export default class BaseRoute extends Route { + @service('container') container; + @service('env') env; @service('repository/permission') permissions; + @service('router') router; /** * Inspects a custom `abilities` array on the router for this route. Every @@ -77,6 +80,10 @@ export default class BaseRoute extends Route { super.setupController(...arguments); } + optionalParams() { + return this.container.get(`location:${this.env.var('locationType')}`).optionalParams(); + } + /** * Adds urldecoding to any wildcard route `params` passed into ember `model` * hooks, plus of course anywhere else where `paramsFor` is used. This means diff --git a/ui/packages/consul-ui/app/routing/single.js b/ui/packages/consul-ui/app/routing/single.js index 4ee0ea35f..05842eb3d 100644 --- a/ui/packages/consul-ui/app/routing/single.js +++ b/ui/packages/consul-ui/app/routing/single.js @@ -13,7 +13,7 @@ export default Route.extend({ typeof repo !== 'undefined' ); const dc = this.modelFor('dc').dc.Name; - const nspace = this.modelFor('nspace').nspace.substr(1); + const nspace = this.optionalParams().nspace; const create = this.isCreate(...arguments); return hash({ dc: dc, diff --git a/ui/packages/consul-ui/app/services/env.js b/ui/packages/consul-ui/app/services/env.js index fa25518b0..bbd9a9c1e 100644 --- a/ui/packages/consul-ui/app/services/env.js +++ b/ui/packages/consul-ui/app/services/env.js @@ -5,7 +5,7 @@ export default class EnvService extends Service { // deprecated // TODO: Remove this elsewhere in the app and use var instead env(key) { - return env(key); + return this.var(key); } var(key) { diff --git a/ui/packages/consul-ui/app/services/repository/nspace/disabled.js b/ui/packages/consul-ui/app/services/repository/nspace/disabled.js index 68437a427..79eff5c63 100644 --- a/ui/packages/consul-ui/app/services/repository/nspace/disabled.js +++ b/ui/packages/consul-ui/app/services/repository/nspace/disabled.js @@ -3,7 +3,7 @@ import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/nspace'; const modelName = 'nspace'; const DEFAULT_NSPACE = 'default'; -export default class DisabledService extends RepositoryService { +export default class NspaceDisabledService extends RepositoryService { getPrimaryKey() { return PRIMARY_KEY; } diff --git a/ui/packages/consul-ui/app/services/repository/nspace/enabled.js b/ui/packages/consul-ui/app/services/repository/nspace/enabled.js index 2e385fd85..0f289f802 100644 --- a/ui/packages/consul-ui/app/services/repository/nspace/enabled.js +++ b/ui/packages/consul-ui/app/services/repository/nspace/enabled.js @@ -1,16 +1,14 @@ import { inject as service } from '@ember/service'; -import { get } from '@ember/object'; -import { env } from 'consul-ui/env'; import RepositoryService from 'consul-ui/services/repository'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/nspace'; const modelName = 'nspace'; -export default class EnabledService extends RepositoryService { - @service('router') - router; +export default class NspaceEnabledService extends RepositoryService { + @service('router') router; + @service('container') container; + @service('env') env; - @service('settings') - settings; + @service('settings') settings; getPrimaryKey() { return PRIMARY_KEY; @@ -34,7 +32,7 @@ export default class EnabledService extends RepositoryService { } authorize(dc, nspace) { - if (!env('CONSUL_ACLS_ENABLED')) { + if (!this.env.var('CONSUL_ACLS_ENABLED')) { return Promise.resolve([ { Resource: 'operator', @@ -48,42 +46,19 @@ export default class EnabledService extends RepositoryService { }); } - getActive() { - let routeParams = {}; - // this is only populated before the model hook as fired, - // it is then deleted after the model hook has finished - const infos = get(this, 'router._router.currentState.router.activeTransition.routeInfos'); - if (typeof infos !== 'undefined') { - infos.forEach(function(item) { - Object.keys(item.params).forEach(function(prop) { - routeParams[prop] = item.params[prop]; - }); - }); - } else { - // this is only populated after the model hook has finished - // - const current = get(this, 'router.currentRoute'); - if (current) { - const nspacedRoute = current.find(function(item, i, arr) { - return item.paramNames.includes('nspace'); - }); - if (typeof nspacedRoute !== 'undefined') { - routeParams.nspace = nspacedRoute.params.nspace; - } - } - } + getActive(paramsNspace) { return this.settings .findBySlug('nspace') .then(function(nspace) { // If we can't figure out the nspace from the URL use // the previously saved nspace and if thats not there // then just use default - return routeParams.nspace || nspace || '~default'; + return paramsNspace || nspace || 'default'; }) .then(nspace => this.settings.persist({ nspace: nspace })) .then(function(item) { return { - Name: item.nspace.substr(1), + Name: item.nspace, }; }); } diff --git a/ui/packages/consul-ui/app/services/routlet.js b/ui/packages/consul-ui/app/services/routlet.js index a66e0820f..72c787cd9 100644 --- a/ui/packages/consul-ui/app/services/routlet.js +++ b/ui/packages/consul-ui/app/services/routlet.js @@ -1,4 +1,4 @@ -import Service from '@ember/service'; +import Service, { inject as service } from '@ember/service'; import { schedule } from '@ember/runloop'; class Outlets { @@ -48,6 +48,10 @@ class Outlets { } const outlets = new Outlets(); export default class RoutletService extends Service { + @service('container') container; + @service('env') env; + @service('router') router; + ready() { return this._transition; } @@ -83,6 +87,42 @@ export default class RoutletService extends Service { return {}; } + paramsFor(name) { + let outletParams = {}; + const outlet = outlets.get(name); + if (typeof outlet !== 'undefined' && typeof outlet.args.params !== 'undefined') { + outletParams = outlet.args.params; + } + const route = this.router.currentRoute; + // TODO: Opportunity to dry out this with transitionable + // walk up the entire route/s replacing any instances + // of the specified params with the values specified + let current = route; + let parent; + let routeParams = {}; + // TODO: Not entirely sure whether we are ok exposing queryParams here + // seeing as accessing them from here means you can get them but not set + // them as yet + // let queryParams = {}; + while ((parent = current.parent)) { + routeParams = { + ...parent.params, + ...routeParams, + }; + // queryParams = { + // ...parent.queryParams, + // ...queryParams + // }; + current = parent; + } + return { + ...this.container.get(`location:${this.env.var('locationType')}`).optionalParams(), + ...routeParams, + // ...queryParams + ...outletParams, + }; + } + addRoute(name, route) { const keys = [...outlets.keys()]; const pos = keys.indexOf(name); diff --git a/ui/packages/consul-ui/app/utils/http/create-query-params.js b/ui/packages/consul-ui/app/utils/http/create-query-params.js index c9d1b6c27..5b4bcd139 100644 --- a/ui/packages/consul-ui/app/utils/http/create-query-params.js +++ b/ui/packages/consul-ui/app/utils/http/create-query-params.js @@ -1,4 +1,4 @@ -export default function(encode) { +export default function(encode = encodeURIComponent) { return function stringify(obj, parent) { return Object.entries(obj) .reduce(function(prev, [key, value], i) { diff --git a/ui/packages/consul-ui/app/utils/routing/transitionable.js b/ui/packages/consul-ui/app/utils/routing/transitionable.js index fdf856322..e1a2098da 100644 --- a/ui/packages/consul-ui/app/utils/routing/transitionable.js +++ b/ui/packages/consul-ui/app/utils/routing/transitionable.js @@ -1,8 +1,4 @@ const filter = function(routeName, atts, params) { - if (typeof params.nspace !== 'undefined' && routeName.startsWith('dc.')) { - routeName = `nspace.${routeName}`; - atts = [params.nspace].concat(atts); - } return [routeName, ...atts]; }; const replaceRouteParams = function(route, params = {}) { @@ -28,7 +24,7 @@ export default function(route, params = {}, container) { atts = atts.concat(replaceRouteParams(parent, params)); current = parent; } - // Reverse atts here so it doen't get confusing whilst debugging + // Reverse atts here so it doesn't get confusing whilst debugging // (.reverse is destructive) atts.reverse(); return filter(route.name || 'application', atts, params); diff --git a/ui/packages/consul-ui/config/environment.js b/ui/packages/consul-ui/config/environment.js index faf050a45..dd2442f73 100644 --- a/ui/packages/consul-ui/config/environment.js +++ b/ui/packages/consul-ui/config/environment.js @@ -41,7 +41,8 @@ module.exports = function(environment, $ = process.env) { modulePrefix: 'consul-ui', environment, rootURL: '/ui/', - locationType: 'auto', + locationType: 'fsm-with-optional', + historySupportMiddleware: true, // We use a complete dynamically (from Consul) configured torii provider. // We provide this object here to prevent ember from giving a log message @@ -116,7 +117,7 @@ module.exports = function(environment, $ = process.env) { switch (true) { case environment === 'test': ENV = Object.assign({}, ENV, { - locationType: 'none', + locationType: 'fsm-with-optional-test', // During testing ACLs default to being turned on operatorConfig: { diff --git a/ui/packages/consul-ui/package.json b/ui/packages/consul-ui/package.json index 165582470..6cfc9df1c 100644 --- a/ui/packages/consul-ui/package.json +++ b/ui/packages/consul-ui/package.json @@ -112,7 +112,6 @@ "ember-decorators": "^6.1.1", "ember-exam": "^4.0.0", "ember-export-application-global": "^2.0.1", - "ember-href-to": "^3.1.0", "ember-in-viewport": "^3.8.1", "ember-inflector": "^4.0.1", "ember-intl": "^5.5.1", diff --git a/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/index.feature b/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/index.feature index 68da2e61f..2efbafc9a 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/index.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/index.feature @@ -1,48 +1,48 @@ -@setupApplicationTest -Feature: dc / acls / auth-methods / index: ACL Auth Methods List - - Scenario: - Given 1 datacenter model with the value "dc-1" - And 3 authMethod models - When I visit the authMethods page for yaml - --- - dc: dc-1 - --- - Then the url should be /dc-1/acls/auth-methods - Then I see 3 authMethod models - And the title should be "Auth Methods - Consul" - Scenario: Searching the Auth Methods - Given 1 datacenter model with the value "dc-1" - And 3 authMethod models from yaml - --- - - Name: kube - DisplayName: minikube - - Name: agent - DisplayName: '' - - Name: node - DisplayName: mininode - --- - When I visit the authMethods page for yaml - --- - dc: dc-1 - --- - Then the url should be /dc-1/acls/auth-methods - Then I see 3 authMethod models - Then I fill in with yaml - --- - s: kube - --- - And I see 1 authMethod model - And I see 1 authMethod model with the name "minikube" - Then I fill in with yaml - --- - s: agent - --- - And I see 1 authMethod model - And I see 1 authMethod model with the name "agent" - Then I fill in with yaml - --- - s: ode - --- - And I see 1 authMethod model - And I see 1 authMethod model with the name "mininode" +@setupApplicationTest +Feature: dc / acls / auth-methods / index: ACL Auth Methods List + + Scenario: + Given 1 datacenter model with the value "dc-1" + And 3 authMethod models + When I visit the authMethods page for yaml + --- + dc: dc-1 + --- + Then the url should be /dc-1/acls/auth-methods + Then I see 3 authMethod models + And the title should be "Auth Methods - Consul" + Scenario: Searching the Auth Methods + Given 1 datacenter model with the value "dc-1" + And 3 authMethod models from yaml + --- + - Name: kube + DisplayName: minikube + - Name: agent + DisplayName: '' + - Name: node + DisplayName: mininode + --- + When I visit the authMethods page for yaml + --- + dc: dc-1 + --- + Then the url should be /dc-1/acls/auth-methods + Then I see 3 authMethod models + Then I fill in with yaml + --- + s: kube + --- + And I see 1 authMethod model + And I see 1 authMethod model with the name "minikube" + Then I fill in with yaml + --- + s: agent + --- + And I see 1 authMethod model + And I see 1 authMethod model with the name "agent" + Then I fill in with yaml + --- + s: ode + --- + And I see 1 authMethod model + And I see 1 authMethod model with the name "mininode" diff --git a/ui/packages/consul-ui/tests/acceptance/dc/acls/tokens/own-no-delete.feature b/ui/packages/consul-ui/tests/acceptance/dc/acls/tokens/own-no-delete.feature index 38375fb2e..7ef9644ed 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/acls/tokens/own-no-delete.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/acls/tokens/own-no-delete.feature @@ -34,7 +34,7 @@ Feature: dc / acls / tokens / own-no-delete: The your current token has no delet And I visit the token page for yaml --- dc: dc-1 - token: ee52203d-989f-4f7a-ab5a-2bef004164ca + token: token --- - Then the url should be /dc-1/acls/tokens/ee52203d-989f-4f7a-ab5a-2bef004164ca + Then the url should be /dc-1/acls/tokens/token Then I don't see confirmDelete diff --git a/ui/packages/consul-ui/tests/helpers/get-nspace-runner.js b/ui/packages/consul-ui/tests/helpers/get-nspace-runner.js index 5e879c49a..173a793cf 100644 --- a/ui/packages/consul-ui/tests/helpers/get-nspace-runner.js +++ b/ui/packages/consul-ui/tests/helpers/get-nspace-runner.js @@ -1,18 +1,10 @@ -import Service from '@ember/service'; export default function(type) { return function(cb, withNspaces, withoutNspaces, container, assert) { let CONSUL_NSPACES_ENABLED = true; - container.owner.register( - 'service:env', - Service.extend({ - env: function() { - return CONSUL_NSPACES_ENABLED; - }, - var: function() { - return CONSUL_NSPACES_ENABLED; - }, - }) - ); + const env = container.owner.lookup('service:env'); + env.var = function() { + return CONSUL_NSPACES_ENABLED; + }; const adapter = container.owner.lookup(`adapter:${type}`); const serializer = container.owner.lookup(`serializer:${type}`); const client = container.owner.lookup('service:client/http'); diff --git a/ui/packages/consul-ui/tests/integration/helpers/href-mut-test.js b/ui/packages/consul-ui/tests/integration/helpers/href-mut-test.js deleted file mode 100644 index e58ed17fd..000000000 --- a/ui/packages/consul-ui/tests/integration/helpers/href-mut-test.js +++ /dev/null @@ -1,15 +0,0 @@ -import { module, skip } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Helper | href-mut', function(hooks) { - setupRenderingTest(hooks); - - // Replace this with your real tests. - skip('it renders', async function(assert) { - await render(hbs`{{href-mut (hash dc=dc-1)}}`); - - assert.equal(this.element.textContent.trim(), ''); - }); -}); diff --git a/ui/packages/consul-ui/tests/lib/page-object/visitable.js b/ui/packages/consul-ui/tests/lib/page-object/visitable.js index e60372109..a6f86c3a2 100644 --- a/ui/packages/consul-ui/tests/lib/page-object/visitable.js +++ b/ui/packages/consul-ui/tests/lib/page-object/visitable.js @@ -1,8 +1,11 @@ -// import { assign } from '../-private/helpers'; -const assign = Object.assign; +import { getContext } from '@ember/test-helpers'; import { getExecutionContext } from 'ember-cli-page-object/-private/execution_context'; +import createQueryParams from 'consul-ui/utils/http/create-query-params'; -import $ from '-jquery'; +const assign = Object.assign; +const QueryParams = { + stringify: createQueryParams(), +}; function fillInDynamicSegments(path, params, encoder) { return path @@ -29,10 +32,9 @@ function fillInDynamicSegments(path, params, encoder) { } function appendQueryParams(path, queryParams) { - if (Object.keys(queryParams).length) { - path += `?${$.param(queryParams)}`; + if (Object.keys(queryParams).length > 0) { + return `${path}?${QueryParams.stringify(queryParams)}`; } - return path; } /** @@ -59,14 +61,14 @@ export function visitable(path, encoder = encodeURIComponent) { let executionContext = getExecutionContext(this); return executionContext.runAsync(context => { - var params; + let params; let fullPath = (function _try(paths) { let path = paths.shift(); if (typeof dynamicSegmentsAndQueryParams.nspace !== 'undefined') { path = `/:nspace${path}`; } params = assign({}, dynamicSegmentsAndQueryParams); - var fullPath; + let fullPath; try { fullPath = fillInDynamicSegments(path, params, encoder); } catch (e) { @@ -78,9 +80,19 @@ export function visitable(path, encoder = encodeURIComponent) { } return fullPath; })(typeof path === 'string' ? [path] : path.slice(0)); + fullPath = appendQueryParams(fullPath, params); - return context.visit(fullPath); + const container = getContext().owner; + const locationType = container.lookup('service:env').var('locationType'); + const location = container.lookup(`location:${locationType}`); + // look for a visit on the current location first before just using + // visit on the current context/app + if (typeof location.visit === 'function') { + return location.visit(fullPath); + } else { + return context.visit(fullPath); + } }); }, }; diff --git a/ui/packages/consul-ui/tests/steps.js b/ui/packages/consul-ui/tests/steps.js index 9ba679438..74bbf2b19 100644 --- a/ui/packages/consul-ui/tests/steps.js +++ b/ui/packages/consul-ui/tests/steps.js @@ -97,16 +97,23 @@ export default function({ const clipboard = function() { return window.localStorage.getItem('clipboard'); }; + const currentURL = function() { + const context = helpers.getContext(); + const locationType = context.owner.lookup('service:env').var('locationType'); + let location = context.owner.lookup(`location:${locationType}`); + return location.getURLFrom(); + }; + models(library, create, setCookie); http(library, respondWith, setCookie); visit(library, pages, utils.setCurrentPage, reset); click(library, utils.find, helpers.click); form(library, utils.find, helpers.fillIn, helpers.triggerKeyEvent, utils.getCurrentPage); - debug(library, assert, helpers.currentURL); + debug(library, assert, currentURL); assertHttp(library, assert, lastNthRequest); assertModel(library, assert, utils.find, utils.getCurrentPage, pauseUntil, pluralize); assertPage(library, assert, utils.find, utils.getCurrentPage, $); - assertDom(library, assert, pauseUntil, helpers.find, helpers.currentURL, clipboard); + assertDom(library, assert, pauseUntil, helpers.find, currentURL, clipboard); assertForm(library, assert, utils.find, utils.getCurrentPage); return library.given(["I'm using a legacy token"], function(number, model, data) { diff --git a/ui/packages/consul-ui/tests/unit/routes/nspace-test.js b/ui/packages/consul-ui/tests/unit/routes/nspace-test.js deleted file mode 100644 index 4c8da4853..000000000 --- a/ui/packages/consul-ui/tests/unit/routes/nspace-test.js +++ /dev/null @@ -1,11 +0,0 @@ -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; - -module('Unit | Route | nspace', function(hooks) { - setupTest(hooks); - - test('it exists', function(assert) { - let route = this.owner.lookup('route:nspace'); - assert.ok(route); - }); -}); diff --git a/ui/packages/consul-ui/tests/unit/utils/routing/transitionable-test.js b/ui/packages/consul-ui/tests/unit/utils/routing/transitionable-test.js index 9eb8acc8e..0fa1aac9a 100644 --- a/ui/packages/consul-ui/tests/unit/utils/routing/transitionable-test.js +++ b/ui/packages/consul-ui/tests/unit/utils/routing/transitionable-test.js @@ -22,25 +22,6 @@ module('Unit | Utility | routing/transitionable', function() { const actual = transitionable(instance, {}); assert.deepEqual(actual, expected); }); - test('it walks up the route tree to resolve all the required parameters whilst nspaced', function(assert) { - const expected = [ - 'nspace.dc.service.instance', - 'team-1', - 'dc-1', - 'service-0', - 'node-0', - 'service-instance-0', - ]; - const dc = makeRoute('dc', { dc: 'dc-1' }); - const service = makeRoute('dc.service', { service: 'service-0' }, dc); - const instance = makeRoute( - 'dc.service.instance', - { node: 'node-0', id: 'service-instance-0' }, - service - ); - const actual = transitionable(instance, { nspace: 'team-1' }); - assert.deepEqual(actual, expected); - }); test('it walks up the route tree to resolve all the required parameters whilst replacing specified params', function(assert) { const expected = [ 'dc.service.instance', diff --git a/ui/yarn.lock b/ui/yarn.lock index d06375df2..5e8fa1277 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -5501,7 +5501,7 @@ ember-cli-babel-plugin-helpers@^1.0.0, ember-cli-babel-plugin-helpers@^1.1.0, em resolved "https://registry.yarnpkg.com/ember-cli-babel-plugin-helpers/-/ember-cli-babel-plugin-helpers-1.1.1.tgz#5016b80cdef37036c4282eef2d863e1d73576879" integrity sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw== -ember-cli-babel@7, ember-cli-babel@^7.0.0, ember-cli-babel@^7.1.2, ember-cli-babel@^7.1.3, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.20.0, ember-cli-babel@^7.20.5, ember-cli-babel@^7.21.0, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.7.3, ember-cli-babel@^7.8.0: +ember-cli-babel@7, ember-cli-babel@^7.0.0, ember-cli-babel@^7.1.3, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.20.0, ember-cli-babel@^7.20.5, ember-cli-babel@^7.21.0, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.7.3, ember-cli-babel@^7.8.0: version "7.26.1" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.1.tgz#d3f06bd9aec8aac9197c5ff4d0b87ff1e4f0d62a" integrity sha512-WEWP3hJSe9CWL22gEWQ+Y3uKMGk1vLoIREUQfJNKrgUUh3l49bnfAamh3ywcAQz31IgzvkLPO8ZTXO4rxnuP4Q== @@ -6173,13 +6173,6 @@ ember-getowner-polyfill@^2.0.0: ember-cli-version-checker "^2.1.0" ember-factory-for-polyfill "^1.3.1" -ember-href-to@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ember-href-to/-/ember-href-to-3.1.0.tgz#704f66c2b555a2685fac9ddc74eb9c95abaf5b8f" - integrity sha512-rV9KWDMHgkQsEXuPQekxZ9BbJ75jJqkErWHzWscjmmYwbrMAFxjAt7/oeuiaDxMqHlatNXA0lTkPDZKEBTxoFQ== - dependencies: - ember-cli-babel "^7.1.2" - ember-in-element-polyfill@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ember-in-element-polyfill/-/ember-in-element-polyfill-1.0.1.tgz#143504445bb4301656a2eaad42644d684f5164dd"