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
|
testem.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
# storybook
|
|
||||||
storybook-static
|
|
||||||
**/.storybook/*.html
|
|
||||||
|
|
||||||
# ember-try
|
# ember-try
|
||||||
.node_modules.ember-try
|
.node_modules.ember-try
|
||||||
bower.json.ember-try
|
bower.json.ember-try
|
||||||
|
|
|
@ -16,6 +16,10 @@ export default class NspaceAbility extends BaseAbility {
|
||||||
}
|
}
|
||||||
|
|
||||||
get canChoose() {
|
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,15 +24,22 @@ as |item index|>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
{{#if (and (env 'CONSUL_NSPACES_ENABLED') (not-eq item.Namespace @nspace))}}
|
{{#if (and (can 'use nspaces') (not-eq item.Namespace @nspace))}}
|
||||||
<a data-test-service-name href={{href-to 'nspace.dc.services.show' (concat '~' item.Namespace) @dc item.Name }}>
|
<a
|
||||||
|
data-test-service-name
|
||||||
|
href={{href-to 'dc.services.show' @dc item.Name
|
||||||
|
params=(hash
|
||||||
|
nspace=item.Namespace
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{{item.Name}}
|
{{item.Name}}
|
||||||
</a>
|
</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a data-test-service-name href={{href-to 'dc.services.show' item.Name}}>
|
<a data-test-service-name href={{href-to 'dc.services.show' item.Name}}>
|
||||||
{{item.Name}}
|
{{item.Name}}
|
||||||
</a>
|
</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<p data-test-service-name>
|
<p data-test-service-name>
|
||||||
{{item.Name}}
|
{{item.Name}}
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<MenuItem
|
<MenuItem
|
||||||
data-test-datacenter-picker
|
data-test-datacenter-picker
|
||||||
class={{concat (if (eq @dc.Name item.Name) 'is-active') (if item.Local ' is-local') }}
|
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">
|
<BlockSlot @name="label">
|
||||||
{{item.Name}}
|
{{item.Name}}
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
{{#each (reject-by 'DeletedAt' nspaces) as |item|}}
|
{{#each (reject-by 'DeletedAt' nspaces) as |item|}}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
class={{if (eq @nspace.Name item.Name) 'is-active'}}
|
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">
|
<BlockSlot @name="label">
|
||||||
{{item.Name}}
|
{{item.Name}}
|
||||||
|
|
|
@ -20,6 +20,7 @@ routes.
|
||||||
| export | Type | Default | Description |
|
| export | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `model` | `Object` | `undefined` | Arbitrary hash of data passed down from the parent route/outlet |
|
| `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
|
```hbs
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -6,5 +6,6 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{yield (hash
|
{{yield (hash
|
||||||
model=model
|
model=this.model
|
||||||
|
params=this.params
|
||||||
)}}
|
)}}
|
|
@ -12,6 +12,10 @@ export default class RouteComponent extends Component {
|
||||||
return this.args.title;
|
return this.args.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get params() {
|
||||||
|
return this.routlet.paramsFor(this.args.name);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
connect() {
|
connect() {
|
||||||
this.routlet.addRoute(this.args.name, this);
|
this.routlet.addRoute(this.args.name, this);
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<a class="topology-metrics-card"
|
<a class="topology-metrics-card"
|
||||||
href={{if
|
href={{if
|
||||||
(and (env 'CONSUL_NSPACES_ENABLED') (not-eq @item.Namespace @service.Namespace))
|
(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)
|
(href-to "dc.services.show.index" @item.Name)
|
||||||
}}
|
}}
|
||||||
data-permission={{service/intention-permissions @item}}
|
data-permission={{service/intention-permissions @item}}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default class ApplicationController extends Controller {
|
||||||
// you potentially have a new namespace
|
// you potentially have a new namespace
|
||||||
// if you do redirect to it
|
// if you do redirect to it
|
||||||
if (nspace !== this.nspace.Name) {
|
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
|
// 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
|
// 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
|
// (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 Helper from '@ember/component/helper';
|
||||||
import { observes } from '@ember-decorators/object';
|
import { inject as service } from '@ember/service';
|
||||||
import { hrefTo as _hrefTo } from 'ember-href-to/helpers/href-to';
|
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 wildcard from 'consul-ui/utils/routing/wildcard';
|
||||||
|
|
||||||
import { routes } from 'consul-ui/router';
|
import { routes } from 'consul-ui/router';
|
||||||
|
|
||||||
const isWildcard = wildcard(routes);
|
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
|
export const hrefTo = function(container, params, hash = {}) {
|
||||||
// href-to's only if you are within a namespace
|
// TODO: consider getting this from @service('router')._router which is
|
||||||
const currentRouteName = router.currentRouteName || '';
|
// private but we don't need getOwner, and it ensures setupRouter is called
|
||||||
if (currentRouteName.startsWith('nspace.') && targetRouteName.startsWith('dc.')) {
|
// How private is 'router:main'? If its less private maybe stick with it?
|
||||||
targetRouteName = `nspace.${targetRouteName}`;
|
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 {
|
export default class HrefToHelper extends Helper {
|
||||||
@service('router') router;
|
@service('router') router;
|
||||||
|
|
||||||
compute(params, hash) {
|
init() {
|
||||||
let href;
|
super.init(...arguments);
|
||||||
try {
|
this.router.on('routeWillChange', this.routeWillChange);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observes('router.currentURL')
|
compute(params, hash) {
|
||||||
onURLChange() {
|
return hrefTo(getOwner(this), params, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
routeWillChange(transition) {
|
||||||
this.recompute();
|
this.recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
this.router.off('routeWillChange', this.routeWillChange);
|
||||||
|
super.willDestroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,5 +27,6 @@ export default class IsHrefHelper extends Helper {
|
||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
this.router.off('routeWillChange', this.routeWillChange);
|
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) {
|
export function initialize(container) {
|
||||||
// patch Route routeName-like methods for navigation to support nspace relative routes
|
const env = container.lookup('service:env');
|
||||||
Route.reopen(
|
if (env.var('CONSUL_NSPACES_ENABLED')) {
|
||||||
['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')) {
|
|
||||||
// enable the nspace repo
|
// enable the nspace repo
|
||||||
['dc', 'settings', 'dc.intentions.edit', 'dc.intentions.create'].forEach(function(item) {
|
['dc', 'settings', 'dc.intentions.edit', 'dc.intentions.create'].forEach(function(item) {
|
||||||
container.inject(`route:${item}`, 'nspacesRepo', 'service:repository/nspace/enabled');
|
container.inject(`route:${item}`, 'nspacesRepo', 'service:repository/nspace/enabled');
|
||||||
container.inject(`route:nspace.${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');
|
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) {
|
use: function(item) {
|
||||||
return this.repo
|
return this.repo
|
||||||
.findBySlug({
|
.findBySlug({
|
||||||
ns: this.modelFor('nspace').nspace.substr(1),
|
|
||||||
dc: this.modelFor('dc').dc.Name,
|
dc: this.modelFor('dc').dc.Name,
|
||||||
|
ns: get(item, 'Namespace'),
|
||||||
id: get(item, 'AccessorID'),
|
id: get(item, 'AccessorID'),
|
||||||
})
|
})
|
||||||
.then(item => {
|
.then(item => {
|
||||||
|
|
|
@ -235,10 +235,6 @@ if (env('CONSUL_NSPACES_ENABLED')) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
routes.nspace = {
|
|
||||||
_options: { path: '/:nspace' },
|
|
||||||
dc: routes.dc,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
runInDebug(() => {
|
runInDebug(() => {
|
||||||
// check to see if we are running docfy and if so add its routes to our
|
// 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([
|
let [token, nspace, dc] = await Promise.all([
|
||||||
this.settingsRepo.findBySlug('token'),
|
this.settingsRepo.findBySlug('token'),
|
||||||
this.nspacesRepo.getActive(),
|
this.nspacesRepo.getActive(this.optionalParams().nspace),
|
||||||
this.repo.findBySlug(params.dc, app.dcs),
|
this.repo.findBySlug(params.dc, app.dcs),
|
||||||
]);
|
]);
|
||||||
// if there is only 1 namespace then use that
|
// if there is only 1 namespace then use that
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default class IndexRoute extends Route {
|
||||||
...this.repo.status({
|
...this.repo.status({
|
||||||
items: this.repo.findAllByDatacenter({
|
items: this.repo.findAllByDatacenter({
|
||||||
dc: this.modelFor('dc').dc.Name,
|
dc: this.modelFor('dc').dc.Name,
|
||||||
ns: this.modelFor('nspace').nspace.substr(1),
|
ns: this.optionalParams().nspace,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
searchProperties: this.queryParams.searchproperty.empty[0],
|
searchProperties: this.queryParams.searchproperty.empty[0],
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default class ShowRoute extends SingleRoute {
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
const dc = this.modelFor('dc').dc;
|
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 super.model(...arguments).then(model => {
|
||||||
return hash({
|
return hash({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Route from 'consul-ui/routing/route';
|
import Route from 'consul-ui/routing/route';
|
||||||
|
|
||||||
export default class AuthMethodRoute extends Route {
|
export default class AuthMethodRoute extends Route {
|
||||||
model() {
|
model(params) {
|
||||||
const parent = this.routeName
|
const parent = this.routeName
|
||||||
.split('.')
|
.split('.')
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class EditRoute extends SingleRoute.extend(WithPolicyActions) {
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.optionalParams().nspace;
|
||||||
const tokenRepo = this.tokenRepo;
|
const tokenRepo = this.tokenRepo;
|
||||||
return super.model(...arguments).then(model => {
|
return super.model(...arguments).then(model => {
|
||||||
return hash({
|
return hash({
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default class IndexRoute extends Route.extend(WithPolicyActions) {
|
||||||
return hash({
|
return hash({
|
||||||
...this.repo.status({
|
...this.repo.status({
|
||||||
items: this.repo.findAllByDatacenter({
|
items: this.repo.findAllByDatacenter({
|
||||||
ns: this.modelFor('nspace').nspace.substr(1),
|
ns: this.optionalParams().nspace,
|
||||||
dc: this.modelFor('dc').dc.Name,
|
dc: this.modelFor('dc').dc.Name,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class EditRoute extends SingleRoute.extend(WithRoleActions) {
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.optionalParams().nspace;
|
||||||
const tokenRepo = this.tokenRepo;
|
const tokenRepo = this.tokenRepo;
|
||||||
return super.model(...arguments).then(model => {
|
return super.model(...arguments).then(model => {
|
||||||
return hash({
|
return hash({
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default class IndexRoute extends Route.extend(WithRoleActions) {
|
||||||
return hash({
|
return hash({
|
||||||
...this.repo.status({
|
...this.repo.status({
|
||||||
items: this.repo.findAllByDatacenter({
|
items: this.repo.findAllByDatacenter({
|
||||||
ns: this.modelFor('nspace').nspace.substr(1),
|
ns: this.optionalParams().nspace,
|
||||||
dc: this.modelFor('dc').dc.Name,
|
dc: this.modelFor('dc').dc.Name,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -33,14 +33,15 @@ export default class IndexRoute extends Route.extend(WithTokenActions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
|
const nspace = this.optionalParams().nspace;
|
||||||
return hash({
|
return hash({
|
||||||
...this.repo.status({
|
...this.repo.status({
|
||||||
items: this.repo.findAllByDatacenter({
|
items: this.repo.findAllByDatacenter({
|
||||||
ns: this.modelFor('nspace').nspace.substr(1),
|
ns: nspace,
|
||||||
dc: this.modelFor('dc').dc.Name,
|
dc: this.modelFor('dc').dc.Name,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
nspace: this.modelFor('nspace').nspace.substr(1),
|
nspace: nspace,
|
||||||
token: this.settings.findBySlug('token'),
|
token: this.settings.findBySlug('token'),
|
||||||
searchProperties: this.queryParams.searchproperty.empty[0],
|
searchProperties: this.queryParams.searchproperty.empty[0],
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,16 +5,16 @@ export default class EditRoute extends Route {
|
||||||
@service('repository/intention') repo;
|
@service('repository/intention') repo;
|
||||||
@service('env') env;
|
@service('env') env;
|
||||||
|
|
||||||
async model({ intention_id }, transition) {
|
async model(params, transition) {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.optionalParams().nspace;
|
||||||
|
|
||||||
let item;
|
let item;
|
||||||
if (typeof intention_id !== 'undefined') {
|
if (typeof params.intention_id !== 'undefined') {
|
||||||
item = await this.repo.findBySlug({
|
item = await this.repo.findBySlug({
|
||||||
ns: nspace,
|
ns: nspace,
|
||||||
dc: dc,
|
dc: dc,
|
||||||
id: intention_id,
|
id: params.intention_id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const defaultNspace = this.env.var('CONSUL_NSPACES_ENABLED') ? '*' : 'default';
|
const defaultNspace = this.env.var('CONSUL_NSPACES_ENABLED') ? '*' : 'default';
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default class IndexRoute extends Route {
|
||||||
async model(params) {
|
async model(params) {
|
||||||
return {
|
return {
|
||||||
dc: this.modelFor('dc').dc.Name,
|
dc: this.modelFor('dc').dc.Name,
|
||||||
nspace: this.modelFor('nspace').nspace.substr(1),
|
nspace: this.optionalParams().nspace,
|
||||||
searchProperties: this.queryParams.searchproperty.empty[0],
|
searchProperties: this.queryParams.searchproperty.empty[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default class EditRoute extends Route {
|
||||||
.indexOf('create') !== -1;
|
.indexOf('create') !== -1;
|
||||||
const key = params.key;
|
const key = params.key;
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.optionalParams().nspace;
|
||||||
return hash({
|
return hash({
|
||||||
dc: dc,
|
dc: dc,
|
||||||
nspace: nspace || 'default',
|
nspace: nspace || 'default',
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default class IndexRoute extends Route {
|
||||||
model(params) {
|
model(params) {
|
||||||
let key = params.key || '/';
|
let key = params.key || '/';
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.optionalParams().nspace;
|
||||||
return hash({
|
return hash({
|
||||||
parent: this.repo.findBySlug({
|
parent: this.repo.findBySlug({
|
||||||
ns: nspace,
|
ns: nspace,
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class IndexRoute extends Route {
|
||||||
|
|
||||||
async model(params) {
|
async model(params) {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
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 items = this.data.source(uri => uri`/${nspace}/${dc}/nodes`);
|
||||||
const leader = this.data.source(uri => uri`/${nspace}/${dc}/leader`);
|
const leader = this.data.source(uri => uri`/${nspace}/${dc}/leader`);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,12 +3,11 @@ import Route from 'consul-ui/routing/route';
|
||||||
import { hash } from 'rsvp';
|
import { hash } from 'rsvp';
|
||||||
|
|
||||||
export default class ShowRoute extends Route {
|
export default class ShowRoute extends Route {
|
||||||
@service('data-source/service')
|
@service('data-source/service') data;
|
||||||
data;
|
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.optionalParams().nspace;
|
||||||
const name = params.name;
|
const name = params.name;
|
||||||
return hash({
|
return hash({
|
||||||
dc: dc,
|
dc: dc,
|
||||||
|
|
|
@ -7,6 +7,7 @@ export default class IndexRoute extends Route {
|
||||||
.split('.')
|
.split('.')
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.join('.');
|
.join('.');
|
||||||
|
model = this.modelFor(parent);
|
||||||
// the default selected tab depends on whether you have any healthchecks or not
|
// the default selected tab depends on whether you have any healthchecks or not
|
||||||
// so check the length here.
|
// so check the length here.
|
||||||
const to = get(model, 'item.Checks.length') > 0 ? 'healthchecks' : 'services';
|
const to = get(model, 'item.Checks.length') > 0 ? 'healthchecks' : 'services';
|
||||||
|
|
|
@ -14,13 +14,13 @@ export default class SessionsRoute extends Route.extend(WithBlockingActions) {
|
||||||
@service('feedback')
|
@service('feedback')
|
||||||
feedback;
|
feedback;
|
||||||
|
|
||||||
model() {
|
model(params) {
|
||||||
const parent = this.routeName
|
const parent = this.routeName
|
||||||
.split('.')
|
.split('.')
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.join('.');
|
.join('.');
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
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;
|
const node = this.paramsFor(parent).name;
|
||||||
return hash({
|
return hash({
|
||||||
dc: dc,
|
dc: dc,
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default class IndexRoute extends Route {
|
||||||
};
|
};
|
||||||
|
|
||||||
async model(params, transition) {
|
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 dc = this.modelFor('dc').dc.Name;
|
||||||
const items = this.data.source(uri => uri`/${nspace}/${dc}/services`);
|
const items = this.data.source(uri => uri`/${nspace}/${dc}/services`);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default class InstanceRoute extends Route {
|
||||||
|
|
||||||
async model(params, transition) {
|
async model(params, transition) {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
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(
|
const item = await this.data.source(
|
||||||
uri => uri`/${nspace}/${dc}/service-instance/${params.id}/${params.node}/${params.name}`
|
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) {
|
async model(params, transition) {
|
||||||
const dc = this.modelFor('dc').dc;
|
const dc = this.modelFor('dc').dc;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.optionalParams().nspace;
|
||||||
const slug = params.name;
|
const slug = params.name;
|
||||||
|
|
||||||
let chain;
|
let chain;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Route from '@ember/routing/route';
|
import Route from 'consul-ui/routing/route';
|
||||||
import { get } from '@ember/object';
|
import { get } from '@ember/object';
|
||||||
|
|
||||||
export default class IndexRoute extends Route {
|
export default class IndexRoute extends Route {
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default class IndexRoute extends Route {
|
||||||
async model(params) {
|
async model(params) {
|
||||||
return {
|
return {
|
||||||
dc: this.modelFor('dc').dc.Name,
|
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,
|
slug: this.paramsFor('dc.services.show').name,
|
||||||
searchProperties: this.queryParams.searchproperty.empty[0],
|
searchProperties: this.queryParams.searchproperty.empty[0],
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class ServicesRoute extends Route {
|
||||||
|
|
||||||
async model(params, transition) {
|
async model(params, transition) {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.optionalParams().nspace;
|
||||||
const parent = this.routeName
|
const parent = this.routeName
|
||||||
.split('.')
|
.split('.')
|
||||||
.slice(0, -1)
|
.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);
|
const isWildcard = wildcard(routes);
|
||||||
|
|
||||||
export default class BaseRoute extends Route {
|
export default class BaseRoute extends Route {
|
||||||
|
@service('container') container;
|
||||||
|
@service('env') env;
|
||||||
@service('repository/permission') permissions;
|
@service('repository/permission') permissions;
|
||||||
|
@service('router') router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inspects a custom `abilities` array on the router for this route. Every
|
* 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);
|
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`
|
* Adds urldecoding to any wildcard route `params` passed into ember `model`
|
||||||
* hooks, plus of course anywhere else where `paramsFor` is used. This means
|
* hooks, plus of course anywhere else where `paramsFor` is used. This means
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default Route.extend({
|
||||||
typeof repo !== 'undefined'
|
typeof repo !== 'undefined'
|
||||||
);
|
);
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
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);
|
const create = this.isCreate(...arguments);
|
||||||
return hash({
|
return hash({
|
||||||
dc: dc,
|
dc: dc,
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default class EnvService extends Service {
|
||||||
// deprecated
|
// deprecated
|
||||||
// TODO: Remove this elsewhere in the app and use var instead
|
// TODO: Remove this elsewhere in the app and use var instead
|
||||||
env(key) {
|
env(key) {
|
||||||
return env(key);
|
return this.var(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
var(key) {
|
var(key) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/nspace';
|
||||||
|
|
||||||
const modelName = 'nspace';
|
const modelName = 'nspace';
|
||||||
const DEFAULT_NSPACE = 'default';
|
const DEFAULT_NSPACE = 'default';
|
||||||
export default class DisabledService extends RepositoryService {
|
export default class NspaceDisabledService extends RepositoryService {
|
||||||
getPrimaryKey() {
|
getPrimaryKey() {
|
||||||
return PRIMARY_KEY;
|
return PRIMARY_KEY;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { inject as service } from '@ember/service';
|
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 RepositoryService from 'consul-ui/services/repository';
|
||||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/nspace';
|
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/nspace';
|
||||||
|
|
||||||
const modelName = 'nspace';
|
const modelName = 'nspace';
|
||||||
export default class EnabledService extends RepositoryService {
|
export default class NspaceEnabledService extends RepositoryService {
|
||||||
@service('router')
|
@service('router') router;
|
||||||
router;
|
@service('container') container;
|
||||||
|
@service('env') env;
|
||||||
|
|
||||||
@service('settings')
|
@service('settings') settings;
|
||||||
settings;
|
|
||||||
|
|
||||||
getPrimaryKey() {
|
getPrimaryKey() {
|
||||||
return PRIMARY_KEY;
|
return PRIMARY_KEY;
|
||||||
|
@ -34,7 +32,7 @@ export default class EnabledService extends RepositoryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
authorize(dc, nspace) {
|
authorize(dc, nspace) {
|
||||||
if (!env('CONSUL_ACLS_ENABLED')) {
|
if (!this.env.var('CONSUL_ACLS_ENABLED')) {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{
|
{
|
||||||
Resource: 'operator',
|
Resource: 'operator',
|
||||||
|
@ -48,42 +46,19 @@ export default class EnabledService extends RepositoryService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getActive() {
|
getActive(paramsNspace) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.settings
|
return this.settings
|
||||||
.findBySlug('nspace')
|
.findBySlug('nspace')
|
||||||
.then(function(nspace) {
|
.then(function(nspace) {
|
||||||
// If we can't figure out the nspace from the URL use
|
// If we can't figure out the nspace from the URL use
|
||||||
// the previously saved nspace and if thats not there
|
// the previously saved nspace and if thats not there
|
||||||
// then just use default
|
// then just use default
|
||||||
return routeParams.nspace || nspace || '~default';
|
return paramsNspace || nspace || 'default';
|
||||||
})
|
})
|
||||||
.then(nspace => this.settings.persist({ nspace: nspace }))
|
.then(nspace => this.settings.persist({ nspace: nspace }))
|
||||||
.then(function(item) {
|
.then(function(item) {
|
||||||
return {
|
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';
|
import { schedule } from '@ember/runloop';
|
||||||
|
|
||||||
class Outlets {
|
class Outlets {
|
||||||
|
@ -48,6 +48,10 @@ class Outlets {
|
||||||
}
|
}
|
||||||
const outlets = new Outlets();
|
const outlets = new Outlets();
|
||||||
export default class RoutletService extends Service {
|
export default class RoutletService extends Service {
|
||||||
|
@service('container') container;
|
||||||
|
@service('env') env;
|
||||||
|
@service('router') router;
|
||||||
|
|
||||||
ready() {
|
ready() {
|
||||||
return this._transition;
|
return this._transition;
|
||||||
}
|
}
|
||||||
|
@ -83,6 +87,42 @@ export default class RoutletService extends Service {
|
||||||
return {};
|
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) {
|
addRoute(name, route) {
|
||||||
const keys = [...outlets.keys()];
|
const keys = [...outlets.keys()];
|
||||||
const pos = keys.indexOf(name);
|
const pos = keys.indexOf(name);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function(encode) {
|
export default function(encode = encodeURIComponent) {
|
||||||
return function stringify(obj, parent) {
|
return function stringify(obj, parent) {
|
||||||
return Object.entries(obj)
|
return Object.entries(obj)
|
||||||
.reduce(function(prev, [key, value], i) {
|
.reduce(function(prev, [key, value], i) {
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
const filter = function(routeName, atts, params) {
|
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];
|
return [routeName, ...atts];
|
||||||
};
|
};
|
||||||
const replaceRouteParams = function(route, params = {}) {
|
const replaceRouteParams = function(route, params = {}) {
|
||||||
|
@ -28,7 +24,7 @@ export default function(route, params = {}, container) {
|
||||||
atts = atts.concat(replaceRouteParams(parent, params));
|
atts = atts.concat(replaceRouteParams(parent, params));
|
||||||
current = parent;
|
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)
|
// (.reverse is destructive)
|
||||||
atts.reverse();
|
atts.reverse();
|
||||||
return filter(route.name || 'application', atts, params);
|
return filter(route.name || 'application', atts, params);
|
||||||
|
|
|
@ -41,7 +41,8 @@ module.exports = function(environment, $ = process.env) {
|
||||||
modulePrefix: 'consul-ui',
|
modulePrefix: 'consul-ui',
|
||||||
environment,
|
environment,
|
||||||
rootURL: '/ui/',
|
rootURL: '/ui/',
|
||||||
locationType: 'auto',
|
locationType: 'fsm-with-optional',
|
||||||
|
historySupportMiddleware: true,
|
||||||
|
|
||||||
// We use a complete dynamically (from Consul) configured torii provider.
|
// We use a complete dynamically (from Consul) configured torii provider.
|
||||||
// We provide this object here to prevent ember from giving a log message
|
// 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) {
|
switch (true) {
|
||||||
case environment === 'test':
|
case environment === 'test':
|
||||||
ENV = Object.assign({}, ENV, {
|
ENV = Object.assign({}, ENV, {
|
||||||
locationType: 'none',
|
locationType: 'fsm-with-optional-test',
|
||||||
|
|
||||||
// During testing ACLs default to being turned on
|
// During testing ACLs default to being turned on
|
||||||
operatorConfig: {
|
operatorConfig: {
|
||||||
|
|
|
@ -112,7 +112,6 @@
|
||||||
"ember-decorators": "^6.1.1",
|
"ember-decorators": "^6.1.1",
|
||||||
"ember-exam": "^4.0.0",
|
"ember-exam": "^4.0.0",
|
||||||
"ember-export-application-global": "^2.0.1",
|
"ember-export-application-global": "^2.0.1",
|
||||||
"ember-href-to": "^3.1.0",
|
|
||||||
"ember-in-viewport": "^3.8.1",
|
"ember-in-viewport": "^3.8.1",
|
||||||
"ember-inflector": "^4.0.1",
|
"ember-inflector": "^4.0.1",
|
||||||
"ember-intl": "^5.5.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
|
And I visit the token page for yaml
|
||||||
---
|
---
|
||||||
dc: dc-1
|
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
|
Then I don't see confirmDelete
|
||||||
|
|
|
@ -1,18 +1,10 @@
|
||||||
import Service from '@ember/service';
|
|
||||||
export default function(type) {
|
export default function(type) {
|
||||||
return function(cb, withNspaces, withoutNspaces, container, assert) {
|
return function(cb, withNspaces, withoutNspaces, container, assert) {
|
||||||
let CONSUL_NSPACES_ENABLED = true;
|
let CONSUL_NSPACES_ENABLED = true;
|
||||||
container.owner.register(
|
const env = container.owner.lookup('service:env');
|
||||||
'service:env',
|
env.var = function() {
|
||||||
Service.extend({
|
return CONSUL_NSPACES_ENABLED;
|
||||||
env: function() {
|
};
|
||||||
return CONSUL_NSPACES_ENABLED;
|
|
||||||
},
|
|
||||||
var: function() {
|
|
||||||
return CONSUL_NSPACES_ENABLED;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const adapter = container.owner.lookup(`adapter:${type}`);
|
const adapter = container.owner.lookup(`adapter:${type}`);
|
||||||
const serializer = container.owner.lookup(`serializer:${type}`);
|
const serializer = container.owner.lookup(`serializer:${type}`);
|
||||||
const client = container.owner.lookup('service:client/http');
|
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';
|
import { getContext } from '@ember/test-helpers';
|
||||||
const assign = Object.assign;
|
|
||||||
import { getExecutionContext } from 'ember-cli-page-object/-private/execution_context';
|
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) {
|
function fillInDynamicSegments(path, params, encoder) {
|
||||||
return path
|
return path
|
||||||
|
@ -29,10 +32,9 @@ function fillInDynamicSegments(path, params, encoder) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendQueryParams(path, queryParams) {
|
function appendQueryParams(path, queryParams) {
|
||||||
if (Object.keys(queryParams).length) {
|
if (Object.keys(queryParams).length > 0) {
|
||||||
path += `?${$.param(queryParams)}`;
|
return `${path}?${QueryParams.stringify(queryParams)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -59,14 +61,14 @@ export function visitable(path, encoder = encodeURIComponent) {
|
||||||
let executionContext = getExecutionContext(this);
|
let executionContext = getExecutionContext(this);
|
||||||
|
|
||||||
return executionContext.runAsync(context => {
|
return executionContext.runAsync(context => {
|
||||||
var params;
|
let params;
|
||||||
let fullPath = (function _try(paths) {
|
let fullPath = (function _try(paths) {
|
||||||
let path = paths.shift();
|
let path = paths.shift();
|
||||||
if (typeof dynamicSegmentsAndQueryParams.nspace !== 'undefined') {
|
if (typeof dynamicSegmentsAndQueryParams.nspace !== 'undefined') {
|
||||||
path = `/:nspace${path}`;
|
path = `/:nspace${path}`;
|
||||||
}
|
}
|
||||||
params = assign({}, dynamicSegmentsAndQueryParams);
|
params = assign({}, dynamicSegmentsAndQueryParams);
|
||||||
var fullPath;
|
let fullPath;
|
||||||
try {
|
try {
|
||||||
fullPath = fillInDynamicSegments(path, params, encoder);
|
fullPath = fillInDynamicSegments(path, params, encoder);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -78,9 +80,19 @@ export function visitable(path, encoder = encodeURIComponent) {
|
||||||
}
|
}
|
||||||
return fullPath;
|
return fullPath;
|
||||||
})(typeof path === 'string' ? [path] : path.slice(0));
|
})(typeof path === 'string' ? [path] : path.slice(0));
|
||||||
|
|
||||||
fullPath = appendQueryParams(fullPath, params);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -97,16 +97,23 @@ export default function({
|
||||||
const clipboard = function() {
|
const clipboard = function() {
|
||||||
return window.localStorage.getItem('clipboard');
|
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);
|
models(library, create, setCookie);
|
||||||
http(library, respondWith, setCookie);
|
http(library, respondWith, setCookie);
|
||||||
visit(library, pages, utils.setCurrentPage, reset);
|
visit(library, pages, utils.setCurrentPage, reset);
|
||||||
click(library, utils.find, helpers.click);
|
click(library, utils.find, helpers.click);
|
||||||
form(library, utils.find, helpers.fillIn, helpers.triggerKeyEvent, utils.getCurrentPage);
|
form(library, utils.find, helpers.fillIn, helpers.triggerKeyEvent, utils.getCurrentPage);
|
||||||
debug(library, assert, helpers.currentURL);
|
debug(library, assert, currentURL);
|
||||||
assertHttp(library, assert, lastNthRequest);
|
assertHttp(library, assert, lastNthRequest);
|
||||||
assertModel(library, assert, utils.find, utils.getCurrentPage, pauseUntil, pluralize);
|
assertModel(library, assert, utils.find, utils.getCurrentPage, pauseUntil, pluralize);
|
||||||
assertPage(library, assert, utils.find, utils.getCurrentPage, $);
|
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);
|
assertForm(library, assert, utils.find, utils.getCurrentPage);
|
||||||
|
|
||||||
return library.given(["I'm using a legacy token"], function(number, model, data) {
|
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, {});
|
const actual = transitionable(instance, {});
|
||||||
assert.deepEqual(actual, expected);
|
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) {
|
test('it walks up the route tree to resolve all the required parameters whilst replacing specified params', function(assert) {
|
||||||
const expected = [
|
const expected = [
|
||||||
'dc.service.instance',
|
'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"
|
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==
|
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"
|
version "7.26.1"
|
||||||
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.1.tgz#d3f06bd9aec8aac9197c5ff4d0b87ff1e4f0d62a"
|
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.1.tgz#d3f06bd9aec8aac9197c5ff4d0b87ff1e4f0d62a"
|
||||||
integrity sha512-WEWP3hJSe9CWL22gEWQ+Y3uKMGk1vLoIREUQfJNKrgUUh3l49bnfAamh3ywcAQz31IgzvkLPO8ZTXO4rxnuP4Q==
|
integrity sha512-WEWP3hJSe9CWL22gEWQ+Y3uKMGk1vLoIREUQfJNKrgUUh3l49bnfAamh3ywcAQz31IgzvkLPO8ZTXO4rxnuP4Q==
|
||||||
|
@ -6173,13 +6173,6 @@ ember-getowner-polyfill@^2.0.0:
|
||||||
ember-cli-version-checker "^2.1.0"
|
ember-cli-version-checker "^2.1.0"
|
||||||
ember-factory-for-polyfill "^1.3.1"
|
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:
|
ember-in-element-polyfill@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ember-in-element-polyfill/-/ember-in-element-polyfill-1.0.1.tgz#143504445bb4301656a2eaad42644d684f5164dd"
|
resolved "https://registry.yarnpkg.com/ember-in-element-polyfill/-/ember-in-element-polyfill-1.0.1.tgz#143504445bb4301656a2eaad42644d684f5164dd"
|
||||||
|
|
Loading…
Reference in New Issue