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:
parent
4b9d29fbdc
commit
7083c39b96
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Add 'optional route segments' and move namespaces to use them
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,15 @@ 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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,5 +6,6 @@
|
|||
{{/if}}
|
||||
|
||||
{{yield (hash
|
||||
model=model
|
||||
model=this.model
|
||||
params=this.params
|
||||
)}}
|
|
@ -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);
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
||||
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('/');
|
||||
});
|
||||
}
|
||||
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}`;
|
||||
return location.hrefTo(routeName, _params, _hash);
|
||||
} catch (e) {
|
||||
if (e.constructor === Error) {
|
||||
e.message = `${e.message} For "${params[0]}:${JSON.stringify(params.slice(1))}"`;
|
||||
}
|
||||
return _hrefTo(owned, [targetRouteName, ...rest]);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,5 +27,6 @@ export default class IsHrefHelper extends Helper {
|
|||
|
||||
willDestroy() {
|
||||
this.router.off('routeWillChange', this.routeWillChange);
|
||||
super.willDestroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}`
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
const env = container.owner.lookup('service:env');
|
||||
env.var = function() {
|
||||
return CONSUL_NSPACES_ENABLED;
|
||||
},
|
||||
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');
|
||||
|
|
|
@ -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(), '');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue