ui: Support Route optional parameters/segments (#10212)

Moves our URLs with 'optional namespace segment' into a separately abstracted 'optional URL segment' feature
This commit is contained in:
John Cowen 2021-05-26 17:43:46 +01:00 committed by GitHub
parent e11454cf71
commit 2e4c9f5330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 849 additions and 451 deletions

3
.changelog/10212.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Add 'optional route segments' and move namespaces to use them
```

4
ui/.gitignore vendored
View File

@ -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

View File

@ -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');
}
}

View File

@ -24,15 +24,22 @@ as |item index|>
</Tooltip>
</dd>
</dl>
{{#if (and (env 'CONSUL_NSPACES_ENABLED') (not-eq item.Namespace @nspace))}}
<a data-test-service-name href={{href-to 'nspace.dc.services.show' (concat '~' item.Namespace) @dc item.Name }}>
{{#if (and (can 'use nspaces') (not-eq item.Namespace @nspace))}}
<a
data-test-service-name
href={{href-to 'dc.services.show' @dc item.Name
params=(hash
nspace=item.Namespace
)
}}
>
{{item.Name}}
</a>
{{else}}
{{else}}
<a data-test-service-name href={{href-to 'dc.services.show' item.Name}}>
{{item.Name}}
</a>
{{/if}}
{{/if}}
{{else}}
<p data-test-service-name>
{{item.Name}}

View File

@ -36,7 +36,7 @@
<MenuItem
data-test-datacenter-picker
class={{concat (if (eq @dc.Name item.Name) 'is-active') (if item.Local ' is-local') }}
@href={{href-mut (hash dc=item.Name)}}
@href={{href-to '.' params=(hash dc=item.Name)}}
>
<BlockSlot @name="label">
{{item.Name}}
@ -81,7 +81,7 @@
{{#each (reject-by 'DeletedAt' nspaces) as |item|}}
<MenuItem
class={{if (eq @nspace.Name item.Name) 'is-active'}}
@href={{href-mut (hash nspace=(concat '~' item.Name))}}
@href={{href-to '.' params=(hash nspace=item.Name)}}
>
<BlockSlot @name="label">
{{item.Name}}

View File

@ -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
<Route

View File

@ -6,5 +6,6 @@
{{/if}}
{{yield (hash
model=model
model=this.model
params=this.params
)}}

View File

@ -12,6 +12,10 @@ export default class RouteComponent extends Component {
return this.args.title;
}
get params() {
return this.routlet.paramsFor(this.args.name);
}
@action
connect() {
this.routlet.addRoute(this.args.name, this);

View File

@ -8,7 +8,7 @@
<a class="topology-metrics-card"
href={{if
(and (env 'CONSUL_NSPACES_ENABLED') (not-eq @item.Namespace @service.Namespace))
(href-to "nspace.dc.services.show.index" (concat '~' @item.Namespace) @item.Datacenter @item.Name)
(href-to "dc.services.show.index" @item.Datacenter @item.Name params=(hash nspace=@item.Namespace))
(href-to "dc.services.show.index" @item.Name)
}}
data-permission={{service/intention-permissions @item}}

View File

@ -40,7 +40,7 @@ export default class ApplicationController extends Controller {
// you potentially have a new namespace
// if you do redirect to it
if (nspace !== this.nspace.Name) {
params.nspace = `~${nspace}`;
params.nspace = `${nspace}`;
}
}
}

View File

@ -1,18 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { hrefTo } from 'consul-ui/helpers/href-to';
import { getOwner } from '@ember/application';
import transitionable from 'consul-ui/utils/routing/transitionable';
export default class HrefMutHelper extends Helper {
@service('router') router;
compute([params], hash) {
return hrefTo(
this,
this.router,
transitionable(this.router.currentRoute, params, getOwner(this)),
hash
);
}
}

View File

@ -1,59 +1,73 @@
/*eslint ember/no-observers: "warn"*/
// TODO: Remove ^
// This helper requires `ember-href-to` for the moment at least
// It's similar code but allows us to check on the type of route
// (dynamic or wildcard) and encode or not depending on the type
import { inject as service } from '@ember/service';
import Helper from '@ember/component/helper';
import { observes } from '@ember-decorators/object';
import { hrefTo as _hrefTo } from 'ember-href-to/helpers/href-to';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { getOwner } from '@ember/application';
import transitionable from 'consul-ui/utils/routing/transitionable';
import wildcard from 'consul-ui/utils/routing/wildcard';
import { routes } from 'consul-ui/router';
const isWildcard = wildcard(routes);
export const hrefTo = function(owned, router, [targetRouteName, ...rest], namedArgs) {
if (isWildcard(targetRouteName)) {
rest = rest.map(function(item, i) {
return item
.split('/')
.map(encodeURIComponent)
.join('/');
});
}
if (namedArgs.params) {
return _hrefTo(owned, namedArgs.params);
} else {
// we don't check to see if nspaces are enabled here as routes
// with beginning with 'nspace' only exist if nspaces are enabled
// this globally converts non-nspaced href-to's to nspace aware
// href-to's only if you are within a namespace
const currentRouteName = router.currentRouteName || '';
if (currentRouteName.startsWith('nspace.') && targetRouteName.startsWith('dc.')) {
targetRouteName = `nspace.${targetRouteName}`;
export const hrefTo = function(container, params, hash = {}) {
// TODO: consider getting this from @service('router')._router which is
// private but we don't need getOwner, and it ensures setupRouter is called
// How private is 'router:main'? If its less private maybe stick with it?
const location = container.lookup('router:main').location;
const router = container.lookup('service:router');
let _params = params.slice(0);
let routeName = _params.shift();
let _hash = hash.params || {};
// a period means use the same routeName we are currently at and therefore
// use transitionable to figure out all the missing params
if (routeName === '.') {
_params = transitionable(router.currentRoute, _hash, container);
// _hash = {};
routeName = _params.shift();
}
try {
// if the routeName is a wildcard (*) route url encode all of the params
if (isWildcard(routeName)) {
_params = _params.map((item, i) => {
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();
}
}

View File

@ -27,5 +27,6 @@ export default class IsHrefHelper extends Helper {
willDestroy() {
this.router.off('routeWillChange', this.routeWillChange);
super.willDestroy();
}
}

View File

@ -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);
},
});
}
},
};

View File

@ -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);
}
});
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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() {}
}

View File

@ -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 => {

View File

@ -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

View File

@ -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

View File

@ -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],

View File

@ -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({

View File

@ -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)

View File

@ -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({

View File

@ -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,
}),
}),

View File

@ -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({

View File

@ -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,
}),
}),

View File

@ -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],
});

View File

@ -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';

View File

@ -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],
};
}

View File

@ -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',

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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';

View File

@ -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,

View File

@ -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 {

View File

@ -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}`

View File

@ -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;

View File

@ -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 {

View File

@ -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],
};

View File

@ -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)

View File

@ -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);
}
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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) {

View File

@ -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;
}

View File

@ -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,
};
});
}

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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: {

View File

@ -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",

View File

@ -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

View File

@ -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');

View File

@ -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(), '');
});
});

View File

@ -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);
}
});
},
};

View File

@ -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) {

View File

@ -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);
});
});

View File

@ -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',

View File

@ -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"