ui: Improved main navigation (#7673)

* Make datacenter queries use query vs findAll like the rest of the app

* Make sure we have an element to pass to isInViewport

* Make sure href-mut doesn't error even if the currentRoute === null

* More post test cleanup and Safari fix (safari requires http:// URLs)

* Reverse order of datasource nspace/dc's and add a namespace source

* Rearrange routes/templates/controllers to only use HashicorpConsul once

* Add datasources and correct token namespace detection/redirection

* Remove old dc findAll adapter method

* Add more comments around the 'child route/parent controller' vars
This commit is contained in:
John Cowen 2020-04-21 16:49:11 +01:00 committed by John Cowen
parent 51db157fab
commit 2685c5081b
22 changed files with 220 additions and 157 deletions

View File

@ -1,7 +1,7 @@
import Adapter from './application'; import Adapter from './application';
export default Adapter.extend({ export default Adapter.extend({
requestForFindAll: function(request) { requestForQuery: function(request) {
return request` return request`
GET /v1/catalog/datacenters GET /v1/catalog/datacenters
`; `;

View File

@ -1,4 +1,4 @@
{{#if (eq loading "lazy")}} {{#if (eq loading "lazy")}}
{{! in order to use intersection observer we need a DOM element on the page}} {{! in order to use intersection observer we need a DOM element on the page}}
<data aria-hidden="true" style="width: 0;height: 0;font-size: 0;padding: 0;margin: 0;" /> <data id={{guid}} aria-hidden="true" style="width: 0;height: 0;font-size: 0;padding: 0;margin: 0;" />
{{/if}} {{/if}}

View File

@ -1,6 +1,7 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { set } from '@ember/object'; import { set } from '@ember/object';
import { schedule } from '@ember/runloop';
import Ember from 'ember'; import Ember from 'ember';
/** /**
@ -25,30 +26,6 @@ const replace = function(
return set(obj, prop, value); return set(obj, prop, value);
}; };
/**
* @module DataSource
*
* The DataSource component manages opening and closing data sources via an injectable data service.
* Data sources are only opened only if the component is visible in the viewport (using IntersectionObserver).
*
* Sources returned by the data service should follow an EventTarget/EventSource API.
* Management of the caching/usage/counting etc of sources should be done in the data service,
* not the component.
*
* @example ```javascript
* <DataSource
* src="/dc-1/~nspace/services"
* onchange={{action (mut items) value='data'}}
* onerror={{action (mut error) value='error'}}
* />```
*
* @param src {string} - An identifier used to determine the source of the data. This is passed
* @param loading {string} - Either `eager` or `lazy`, lazy will only load the data once the component
* is in the viewport
* @param onchange {function=} - An action called when the data changes.
* @param onerror {function=} - An action called on error
*
*/
export default Component.extend({ export default Component.extend({
tagName: '', tagName: '',
@ -67,6 +44,7 @@ export default Component.extend({
this._super(...arguments); this._super(...arguments);
this._listeners = this.dom.listeners(); this._listeners = this.dom.listeners();
this._lazyListeners = this.dom.listeners(); this._lazyListeners = this.dom.listeners();
this.guid = this.dom.guid(this);
}, },
willDestroy: function() { willDestroy: function() {
this.actions.close.apply(this); this.actions.close.apply(this);
@ -78,7 +56,7 @@ export default Component.extend({
this._super(...arguments); this._super(...arguments);
if (this.loading === 'lazy') { if (this.loading === 'lazy') {
this._lazyListeners.add( this._lazyListeners.add(
this.dom.isInViewport(this.element, inViewport => { this.dom.isInViewport(this.dom.element(`#${this.guid}`), inViewport => {
set(this, 'isIntersecting', inViewport || Ember.testing); set(this, 'isIntersecting', inViewport || Ember.testing);
if (!this.isIntersecting) { if (!this.isIntersecting) {
this.actions.close.bind(this)(); this.actions.close.bind(this)();
@ -130,11 +108,13 @@ export default Component.extend({
if (typeof source.getCurrentEvent === 'function') { if (typeof source.getCurrentEvent === 'function') {
const currentEvent = source.getCurrentEvent(); const currentEvent = source.getCurrentEvent();
if (currentEvent) { if (currentEvent) {
try { schedule('afterRender', () => {
this.onchange(currentEvent); try {
} catch (err) { this.onchange(currentEvent);
error(err); } catch (err) {
} error(err);
}
});
} }
} }
}, },

View File

@ -1,3 +1,4 @@
<header role="banner" data-test-navigation> <header role="banner" data-test-navigation>
<a data-test-main-nav-logo href={{href-to 'index'}}><svg width="28" height="27" xmlns="http://www.w3.org/2000/svg"><title>Consul</title><path d="M13.284 16.178a2.876 2.876 0 1 1-.008-5.751 2.876 2.876 0 0 1 .008 5.75zm5.596-1.547a1.333 1.333 0 1 1 0-2.667 1.333 1.333 0 0 1 0 2.667zm4.853 1.249a1.271 1.271 0 1 1 .027-.107c0 .031 0 .067-.027.107zm-.937-3.436a1.333 1.333 0 1 1 .986-1.595c.033.172.033.348 0 .52-.07.53-.465.96-.986 1.075zm4.72 3.29a1.333 1.333 0 1 1-1.076-1.538 1.333 1.333 0 0 1 1.116 1.417.342.342 0 0 0-.027.12h-.013zm-1.08-3.33a1.333 1.333 0 1 1 1.088-1.524c.014.114.014.229 0 .342a1.333 1.333 0 0 1-1.102 1.182h.014zm-.925 7.925a1.333 1.333 0 1 1 .165-.547c-.01.193-.067.38-.165.547zm-.48-12.191a1.333 1.333 0 1 1 .507-1.814c.14.237.198.514.164.787-.038.438-.289.828-.67 1.045v-.018zM13.333 26.667C5.97 26.667 0 20.697 0 13.333 0 5.97 5.97 0 13.333 0c2.929-.01 5.778.955 8.098 2.742L19.8 4.89a10.667 10.667 0 0 0-17.133 8.444 10.667 10.667 0 0 0 17.137 8.471l1.627 2.13a13.218 13.218 0 0 1-8.098 2.733z" fill="#FFF"/></svg></a> <a data-test-main-nav-logo href={{href-to 'index'}}><svg width="28" height="27" xmlns="http://www.w3.org/2000/svg"><title>Consul</title><path d="M13.284 16.178a2.876 2.876 0 1 1-.008-5.751 2.876 2.876 0 0 1 .008 5.75zm5.596-1.547a1.333 1.333 0 1 1 0-2.667 1.333 1.333 0 0 1 0 2.667zm4.853 1.249a1.271 1.271 0 1 1 .027-.107c0 .031 0 .067-.027.107zm-.937-3.436a1.333 1.333 0 1 1 .986-1.595c.033.172.033.348 0 .52-.07.53-.465.96-.986 1.075zm4.72 3.29a1.333 1.333 0 1 1-1.076-1.538 1.333 1.333 0 0 1 1.116 1.417.342.342 0 0 0-.027.12h-.013zm-1.08-3.33a1.333 1.333 0 1 1 1.088-1.524c.014.114.014.229 0 .342a1.333 1.333 0 0 1-1.102 1.182h.014zm-.925 7.925a1.333 1.333 0 1 1 .165-.547c-.01.193-.067.38-.165.547zm-.48-12.191a1.333 1.333 0 1 1 .507-1.814c.14.237.198.514.164.787-.038.438-.289.828-.67 1.045v-.018zM13.333 26.667C5.97 26.667 0 20.697 0 13.333 0 5.97 5.97 0 13.333 0c2.929-.01 5.778.955 8.098 2.742L19.8 4.89a10.667 10.667 0 0 0-17.133 8.444 10.667 10.667 0 0 0 17.137 8.471l1.627 2.13a13.218 13.218 0 0 1-8.098 2.733z" fill="#FFF"/></svg></a>
<input type="checkbox" name="menu" id="main-nav-toggle" onchange={{action 'change'}} /> <input type="checkbox" name="menu" id="main-nav-toggle" onchange={{action 'change'}} />
@ -30,6 +31,11 @@
<BlockSlot @name="menu"> <BlockSlot @name="menu">
<li role="separator"> <li role="separator">
Namespaces Namespaces
<DataSource
@src="/*/*/namespaces"
@onchange={{action (mut nspaces) value="data"}}
@loading="lazy"
/>
</li> </li>
{{#each (reject-by 'DeletedAt' nspaces) as |item|}} {{#each (reject-by 'DeletedAt' nspaces) as |item|}}
<li role="none" class={{if (eq nspace.Name item.Name) 'is-active'}}> <li role="none" class={{if (eq nspace.Name item.Name) 'is-active'}}>
@ -58,6 +64,11 @@
<BlockSlot @name="menu"> <BlockSlot @name="menu">
<li role="separator"> <li role="separator">
Datacenters Datacenters
<DataSource
@src="/*/*/datacenters"
@onchange={{action (mut dcs) value="data"}}
@loading="lazy"
/>
</li> </li>
{{#each dcs as |item|}} {{#each dcs as |item|}}
<li role="none" data-test-datacenter-picker class={{if (eq dc.Name item.Name) 'is-active'}}> <li role="none" data-test-datacenter-picker class={{if (eq dc.Name item.Name) 'is-active'}}>

View File

@ -38,11 +38,17 @@ export default Component.extend({
} else { } else {
// TODO: Ideally we wouldn't need to use env() at a component level // TODO: Ideally we wouldn't need to use env() at a component level
// transitionTo should probably remove it instead if NSPACES aren't enabled // transitionTo should probably remove it instead if NSPACES aren't enabled
if (this.env.var('CONSUL_NSPACES_ENABLED') && get(token, 'Namespace') !== this.nspace) { if (this.env.var('CONSUL_NSPACES_ENABLED') && get(token, 'Namespace') !== this.nspace.Name) {
if (!routeName.startsWith('nspace')) { if (!routeName.startsWith('nspace')) {
routeName = `nspace.${routeName}`; routeName = `nspace.${routeName}`;
} }
return route.transitionTo(`${routeName}`, `~${get(token, 'Namespace')}`, this.dc.Name); const nspace = get(token, 'Namespace');
// you potentially have a new namespace
if (typeof nspace !== 'undefined') {
return route.transitionTo(`${routeName}`, `~${nspace}`, this.dc.Name);
}
// you are logging out, just refresh
return route.refresh();
} else { } else {
if (route.routeName === 'dc.acls.index') { if (route.routeName === 'dc.acls.index') {
return route.transitionTo('dc.acls.tokens.index'); return route.transitionTo('dc.acls.tokens.index');

View File

@ -0,0 +1,3 @@
import Controller from '@ember/controller';
export default Controller.extend({});

View File

@ -10,16 +10,38 @@ const removeLoading = function($from) {
}; };
export default Route.extend(WithBlockingActions, { export default Route.extend(WithBlockingActions, {
dom: service('dom'), dom: service('dom'),
router: service('router'),
nspacesRepo: service('repository/nspace/disabled'), nspacesRepo: service('repository/nspace/disabled'),
repo: service('repository/dc'), repo: service('repository/dc'),
settings: service('settings'), settings: service('settings'),
model: function() {
return hash({
router: this.router,
dcs: this.repo.findAll(),
nspaces: this.nspacesRepo.findAll(),
// these properties are added to the controller from route/dc
// as we don't have access to the dc and nspace params in the URL
// until we get to the route/dc route
// permissions also requires the dc param
// dc: null,
// nspace: null
// token: null
// permissions: null
});
},
setupController: function(controller, model) {
controller.setProperties(model);
},
actions: { actions: {
loading: function(transition, originRoute) { loading: function(transition, originRoute) {
const $root = this.dom.root(); const $root = this.dom.root();
let dc = null; let dc = null;
if (originRoute.routeName !== 'dc') { if (originRoute.routeName !== 'dc' && originRoute.routeName !== 'application') {
const model = this.modelFor('dc') || { dcs: null, dc: { Name: null } }; const app = this.modelFor('application');
dc = this.repo.getActive(model.dc.Name, model.dcs); const model = this.modelFor('dc') || { dc: { Name: null } };
dc = this.repo.getActive(model.dc.Name, app.dcs);
} }
hash({ hash({
loading: !$root.classList.contains('ember-loading'), loading: !$root.classList.contains('ember-loading'),
@ -50,8 +72,6 @@ export default Route.extend(WithBlockingActions, {
error = e.errors[0]; error = e.errors[0];
error.message = error.title || error.detail || 'Error'; error.message = error.title || error.detail || 'Error';
} }
// Try and get the currently attempted dc, whereever that may be
const model = this.modelFor('dc') || this.modelFor('nspace.dc');
// TODO: Unfortunately ember will not maintain the correct URL // TODO: Unfortunately ember will not maintain the correct URL
// for you i.e. when this happens the URL in your browser location bar // for you i.e. when this happens the URL in your browser location bar
// will be the URL where you clicked on the link to come here // will be the URL where you clicked on the link to come here
@ -79,26 +99,46 @@ export default Route.extend(WithBlockingActions, {
if (error.status === '') { if (error.status === '') {
error.message = 'Error'; error.message = 'Error';
} }
// Try and get the currently attempted dc, whereever that may be
let model = this.modelFor('dc') || this.modelFor('nspace.dc');
if (!model) {
const path = new URL(location.href).pathname
.substr(this.router.rootURL.length - 1)
.split('/')
.slice(1, 3);
model = {
nspace: { Name: 'default' },
};
if (path[0].startsWith('~')) {
model.nspace = {
Name: path.shift(),
};
}
model.dc = {
Name: path[0],
};
}
const app = this.modelFor('application') || {};
const dcs = app.dcs || [model.dc];
const nspaces = app.nspaces || [model.nspace];
const $root = this.dom.root(); const $root = this.dom.root();
hash({ hash({
error: error,
nspace: this.nspacesRepo.getActive(),
dc: dc:
error.status.toString().indexOf('5') !== 0 error.status.toString().indexOf('5') !== 0
? this.repo.getActive() ? this.repo.getActive(model.dc.Name, dcs)
: model && model.dc
? model.dc
: { Name: 'Error' }, : { Name: 'Error' },
dcs: model && model.dcs ? model.dcs : [], dcs: dcs,
nspace: model.nspace,
nspaces: nspaces,
}) })
.then(model => Promise.all([model, this.repo.clearActive()])) .then(model => Promise.all([model, this.repo.clearActive()]))
.then(([model]) => { .then(([model]) => {
removeLoading($root); removeLoading($root);
model.nspaces = [model.nspace];
// we can't use setupController as we received an error // we can't use setupController as we received an error
// so we do it manually instead // so we do it manually instead
next(() => { next(() => {
this.controllerFor('error').setProperties(model); this.controllerFor('application').setProperties(model);
this.controllerFor('error').setProperties({ error: error });
}); });
}) })
.catch(e => { .catch(e => {

View File

@ -28,37 +28,33 @@ export default Route.extend({
nspacesRepo: service('repository/nspace/disabled'), nspacesRepo: service('repository/nspace/disabled'),
settingsRepo: service('settings'), settingsRepo: service('settings'),
model: function(params) { model: function(params) {
const repo = this.repo; const app = this.modelFor('application');
const nspacesRepo = this.nspacesRepo;
const settingsRepo = this.settingsRepo;
return hash({ return hash({
dcs: repo.findAll(), nspace: this.nspacesRepo.getActive(),
nspaces: nspacesRepo.findAll(), token: this.settingsRepo.findBySlug('token'),
nspace: nspacesRepo.getActive(), dc: this.repo.findBySlug(params.dc, app.dcs),
token: settingsRepo.findBySlug('token'),
}) })
.then(function(model) { .then(function(model) {
return hash({ return hash({
...model, ...model,
...{ ...{
dc: repo.findBySlug(params.dc, model.dcs),
// if there is only 1 namespace then use that // if there is only 1 namespace then use that
// otherwise find the namespace object that corresponds // otherwise find the namespace object that corresponds
// to the active one // to the active one
nspace: nspace:
model.nspaces.length > 1 app.nspaces.length > 1
? findActiveNspace(model.nspaces, model.nspace) ? findActiveNspace(app.nspaces, model.nspace)
: model.nspaces.firstObject, : app.nspaces.firstObject,
}, },
}); });
}) })
.then(function(model) { .then(model => {
if (get(model, 'token.SecretID')) { if (get(model, 'token.SecretID')) {
return hash({ return hash({
...model, ...model,
...{ ...{
// When disabled nspaces is [], so nspace is undefined // When disabled nspaces is [], so nspace is undefined
permissions: nspacesRepo.authorize(params.dc, get(model, 'nspace.Name')), permissions: this.nspacesRepo.authorize(params.dc, get(model, 'nspace.Name')),
}, },
}); });
} else { } else {
@ -67,7 +63,11 @@ export default Route.extend({
}); });
}, },
setupController: function(controller, model) { setupController: function(controller, model) {
controller.setProperties(model); // the model here is actually required for the entire application
// but we need to wait until we are in this route so we know what the dc
// and or nspace is if the below changes please revists the comments
// in routes/application:model
this.controllerFor('application').setProperties(model);
}, },
actions: { actions: {
// TODO: This will eventually be deprecated please see // TODO: This will eventually be deprecated please see
@ -85,15 +85,13 @@ export default Route.extend({
// including your permissions for being able to manage namespaces // including your permissions for being able to manage namespaces
// Potentially we should just do this on every single transition // Potentially we should just do this on every single transition
// but then we would need to check to see if nspaces are enabled // but then we would need to check to see if nspaces are enabled
const controller = this.controllerFor('application');
Promise.all([ Promise.all([
this.nspacesRepo.findAll(), this.nspacesRepo.findAll(),
this.nspacesRepo.authorize( this.nspacesRepo.authorize(get(controller, 'dc.Name'), get(controller, 'nspace.Name')),
get(this.controller, 'dc.Name'),
get(this.controller, 'nspace.Name')
),
]).then(([nspaces, permissions]) => { ]).then(([nspaces, permissions]) => {
if (typeof this.controller !== 'undefined') { if (typeof controller !== 'undefined') {
this.controller.setProperties({ controller.setProperties({
nspaces: nspaces, nspaces: nspaces,
permissions: permissions, permissions: permissions,
}); });

View File

@ -44,17 +44,24 @@ export default Route.extend({
nspace: params.nspace, nspace: params.nspace,
}); });
}, },
afterModel: function(params) {
// We need to redirect if someone doesn't specify /**
// the section they want, but not redirect if the 'section' is * We need to redirect if someone doesn't specify the section they want,
// specified (i.e. /dc-1/ vs /dc-1/services) * but not redirect if the 'section' is specified
// check how many parts are in the URL to figure this out * (i.e. /dc-1/ vs /dc-1/services).
// if there is a better way to do this then would be good to change *
if (this.router.currentURL.split('/').length < 4) { * If the target route of the transition is `nspace.index`, it means that
if (!params.nspace.startsWith('~')) { * someone didn't specify a section and thus we forward them on to a
this.transitionTo('dc.services', params.nspace); * 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 { } else {
this.transitionTo('nspace.dc.services', params.nspace, params.item.Name); this.transitionTo('dc.services', model.nspace);
} }
} }
}, },

View File

@ -9,21 +9,16 @@ export default Route.extend({
dcRepo: service('repository/dc'), dcRepo: service('repository/dc'),
nspacesRepo: service('repository/nspace/disabled'), nspacesRepo: service('repository/nspace/disabled'),
model: function(params) { model: function(params) {
const app = this.modelFor('application');
return hash({ return hash({
item: this.repo.findAll(), item: this.repo.findAll(),
dcs: this.dcRepo.findAll(), dc: this.dcRepo.getActive(undefined, app.dcs),
nspaces: this.nspacesRepo.findAll(),
nspace: this.nspacesRepo.getActive(), nspace: this.nspacesRepo.getActive(),
}).then(model => { }).then(model => {
if (typeof get(model.item, 'client.blocking') === 'undefined') { if (typeof get(model.item, 'client.blocking') === 'undefined') {
set(model, 'item.client', { blocking: true }); set(model, 'item.client', { blocking: true });
} }
return hash({ return model;
...model,
...{
dc: this.dcRepo.getActive(null, model.dcs),
},
});
}); });
}, },
setupController: function(controller, model) { setupController: function(controller, model) {

View File

@ -2,9 +2,14 @@ import Serializer from './application';
export default Serializer.extend({ export default Serializer.extend({
primaryKey: 'Name', primaryKey: 'Name',
respondForQuery: function(respond, query) {
return respond(function(headers, body) {
return body;
});
},
normalizePayload: function(payload, id, requestType) { normalizePayload: function(payload, id, requestType) {
switch (requestType) { switch (requestType) {
case 'findAll': case 'query':
return payload.map(item => { return payload.map(item => {
return { return {
[this.primaryKey]: item, [this.primaryKey]: item,

View File

@ -3,10 +3,11 @@ import { get } from '@ember/object';
export default Service.extend({ export default Service.extend({
datacenters: service('repository/dc'), datacenters: service('repository/dc'),
namespaces: service('repository/nspace'),
token: service('repository/token'), token: service('repository/token'),
type: service('data-source/protocols/http/blocking'), type: service('data-source/protocols/http/blocking'),
source: function(src, configuration) { source: function(src, configuration) {
const [, dc /*nspace*/, , model, ...rest] = src.split('/'); const [, , /*nspace*/ dc, model, ...rest] = src.split('/');
let find; let find;
const repo = this[model]; const repo = this[model];
if (typeof repo.reconcile === 'function') { if (typeof repo.reconcile === 'function') {
@ -25,6 +26,9 @@ export default Service.extend({
case 'datacenters': case 'datacenters':
find = configuration => repo.findAll(configuration); find = configuration => repo.findAll(configuration);
break; break;
case 'namespaces':
find = configuration => repo.findAll(configuration);
break;
case 'token': case 'token':
find = configuration => repo.self(rest[1], dc); find = configuration => repo.self(rest[1], dc);
break; break;

View File

@ -25,6 +25,12 @@ export default Service.extend({
}, },
willDestroy: function() { willDestroy: function() {
this._listeners.remove(); this._listeners.remove();
Object.entries(sources || {}).forEach(function([key, item]) {
item.close();
});
cache = null;
sources = null;
usage = null;
}, },
open: function(uri, ref) { open: function(uri, ref) {
@ -35,13 +41,10 @@ export default Service.extend({
uri = `consul://${uri}`; uri = `consul://${uri}`;
} }
if (!sources.has(uri)) { if (!sources.has(uri)) {
const url = new URL(uri); let [providerName, pathname] = uri.split('://');
let pathname = url.pathname;
if (pathname.startsWith('//')) { if (pathname.startsWith('//')) {
pathname = pathname.substr(2); pathname = pathname.substr(2);
} }
const providerName = url.protocol.substr(0, url.protocol.length - 1);
const provider = this[providerName]; const provider = this[providerName];
let configuration = {}; let configuration = {};

View File

@ -10,7 +10,7 @@ export default RepositoryService.extend({
return modelName; return modelName;
}, },
findAll: function() { findAll: function() {
return this.store.findAll(this.getModelName()).then(function(items) { return this.store.query(this.getModelName(), {}).then(function(items) {
// TODO: Move to view/template // TODO: Move to view/template
return items.sortBy('Name'); return items.sortBy('Name');
}); });

View File

@ -103,6 +103,9 @@ export default LazyProxyService.extend({
cache = createCache(cacheMap); cache = createCache(cacheMap);
}, },
willDestroy: function() { willDestroy: function() {
Object.entries(cacheMap || {}).forEach(function([key, item]) {
item.close();
});
cacheMap = null; cacheMap = null;
cache = null; cache = null;
}, },

View File

@ -1,13 +1,15 @@
<HeadLayout /> <HeadLayout />
{{title 'Consul' separator=' - '}} {{title 'Consul' separator=' - '}}
{{#if (not-eq router.currentRouteName 'application')}}
<HashicorpConsul @id="wrapper" @permissions={{permissions}} @dcs={{dcs}} @dc={{or dc dcs.firstObject}} @nspaces={{nspaces}} @nspace={{or nspace nspaces.firstObject}}>
{{#if (not loading)}} {{#if (not loading)}}
{{outlet}} {{outlet}}
{{else}} {{else}}
<HashicorpConsul @id="wrapper" @permissions={{permissions}} @dcs={{dcs}} @dc={{dc}} @nspaces={{nspaces}} @nspace={{nspace}}>
<AppView @class="loading show"> <AppView @class="loading show">
<BlockSlot @name="content"> <BlockSlot @name="content">
{{partial 'consul-loading'}} {{partial 'consul-loading'}}
</BlockSlot> </BlockSlot>
</AppView> </AppView>
{{/if}}
</HashicorpConsul> </HashicorpConsul>
{{/if}} {{/if}}

View File

@ -1,3 +1 @@
<HashicorpConsul @id="wrapper" @permissions={{permissions}} @dcs={{dcs}} @dc={{dc}} @nspaces={{nspaces}} @nspace={{nspace}}> {{outlet}}
{{outlet}}
</HashicorpConsul>

View File

@ -1,21 +1,19 @@
<HashicorpConsul @id="wrapper" @permissions={{permissions}} @dcs={{dcs}} @dc={{dc}} @nspaces={{nspaces}} @nspace={{nspace}}> <AppView @class="error show">
<AppView @class="error show"> <BlockSlot @name="header">
<BlockSlot @name="header"> <h1 data-test-error>
<h1 data-test-error>
{{#if error.status }} {{#if error.status }}
{{error.status}} ({{error.message}}) {{error.status}} ({{error.message}})
{{else}} {{else}}
{{error.message}} {{error.message}}
{{/if}} {{/if}}
</h1> </h1>
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">
<p> <p>
Consul returned an error. Consul returned an error.
You may have visited a URL that is loading an unknown resource, so you can try going back to the root or try re-submitting your ACL Token/SecretID by going back to ACLs.<br /> You may have visited a URL that is loading an unknown resource, so you can try going back to the root or try re-submitting your ACL Token/SecretID by going back to ACLs.<br />
Try looking in our <a href="{{env 'CONSUL_DOCS_URL'}}" target="_blank">documentation</a> Try looking in our <a href="{{env 'CONSUL_DOCS_URL'}}" target="_blank">documentation</a>
</p> </p>
<a data-test-home rel="home" href={{href-to 'index'}}>Go back to root</a> <a data-test-home rel="home" href={{href-to 'index'}}>Go back to root</a>
</BlockSlot> </BlockSlot>
</AppView> </AppView>
</HashicorpConsul>

View File

@ -1,43 +1,41 @@
{{title "Settings"}} {{title "Settings"}}
<HashicorpConsul @id="wrapper" @permissions={{permissions}} @dcs={{dcs}} @dc={{dc}} @nspaces={{nspaces}} @nspace={{nspace}}> <AppView @class="settings show">
<AppView @class="settings show"> <BlockSlot @name="header">
<BlockSlot @name="header"> <h1>
<h1> Settings
Settings </h1>
</h1> </BlockSlot>
</BlockSlot> <BlockSlot @name="content">
<BlockSlot @name="content"> <div class="notice info">
<div class="notice info"> <h3>Local Storage</h3>
<h3>Local Storage</h3> <p>
These settings are immediately saved to local storage and persisted through browser usage.
</p>
</div>
<form>
<fieldset>
<h2>Dashboard Links</h2>
<p> <p>
These settings are immediately saved to local storage and persisted through browser usage. Add a link to the service detail page in the UI to get quick access to a service-wide metrics dashboard. Enter the dashboard URL into the field below. You can use the placeholders <code>{{'{{Datacenter}}'}}</code> and <code>{{'{{Service.Name}}'}}</code> which will be replaced with the name of the datacenter/service currently being viewed.
</p> </p>
</div> <label class={{concat (if confirming 'confirming') ' type-text'}} id="urls_service">
<form> <span>Link template for services</span>
<fieldset> <input type="text" name="urls[service]" value={{item.urls.service}} onchange={{action 'change'}} onkeypress={{action 'key'}} onkeydown={{action 'key'}} />
<h2>Dashboard Links</h2> <em>e.g. https://grafana.example.com/d/1/consul-service-mesh&amp;orgid=1&amp;datacenter={{'{{Datacenter}}'}}&amp;service-name={{'{{Service.Name}}'}}</em>
<p> </label>
Add a link to the service detail page in the UI to get quick access to a service-wide metrics dashboard. Enter the dashboard URL into the field below. You can use the placeholders <code>{{'{{Datacenter}}'}}</code> and <code>{{'{{Service.Name}}'}}</code> which will be replaced with the name of the datacenter/service currently being viewed. </fieldset>
</p> {{#if (not (env 'CONSUL_UI_DISABLE_REALTIME'))}}
<label class={{concat (if confirming 'confirming') ' type-text'}} id="urls_service"> <fieldset data-test-blocking-queries>
<span>Link template for services</span> <h2>Blocking Queries</h2>
<input type="text" name="urls[service]" value={{item.urls.service}} onchange={{action 'change'}} onkeypress={{action 'key'}} onkeydown={{action 'key'}} /> <p>Keep catalog info up-to-date without refreshing the page. Any changes made to services, nodes and intentions would be reflected in real time.</p>
<em>e.g. https://grafana.example.com/d/1/consul-service-mesh&amp;orgid=1&amp;datacenter={{'{{Datacenter}}'}}&amp;service-name={{'{{Service.Name}}'}}</em> <div class="type-toggle">
<label>
<input type="checkbox" name="client[blocking]" checked={{if item.client.blocking 'checked'}} onchange={{action 'change'}} />
<span>{{if item.client.blocking 'On' 'Off'}}</span>
</label> </label>
</fieldset> </div>
{{#if (not (env 'CONSUL_UI_DISABLE_REALTIME'))}} </fieldset>
<fieldset data-test-blocking-queries> {{/if}}
<h2>Blocking Queries</h2> </form>
<p>Keep catalog info up-to-date without refreshing the page. Any changes made to services, nodes and intentions would be reflected in real time.</p> </BlockSlot>
<div class="type-toggle"> </AppView>
<label>
<input type="checkbox" name="client[blocking]" checked={{if item.client.blocking 'checked'}} onchange={{action 'change'}} />
<span>{{if item.client.blocking 'On' 'Off'}}</span>
</label>
</div>
</fieldset>
{{/if}}
</form>
</BlockSlot>
</AppView>
</HashicorpConsul>

View File

@ -2,11 +2,11 @@ import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit'; import { setupTest } from 'ember-qunit';
module('Integration | Adapter | dc', function(hooks) { module('Integration | Adapter | dc', function(hooks) {
setupTest(hooks); setupTest(hooks);
test('requestForFindAll returns the correct url', function(assert) { test('requestForQuery returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:dc'); const adapter = this.owner.lookup('adapter:dc');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/catalog/datacenters`; const expected = `GET /v1/catalog/datacenters`;
const actual = adapter.requestForFindAll(client.url); const actual = adapter.requestForQuery(client.url);
assert.equal(actual, expected); assert.equal(actual, expected);
}); });
}); });

View File

@ -3,14 +3,14 @@ import { setupTest } from 'ember-qunit';
import { get } from 'consul-ui/tests/helpers/api'; import { get } from 'consul-ui/tests/helpers/api';
module('Integration | Serializer | dc', function(hooks) { module('Integration | Serializer | dc', function(hooks) {
setupTest(hooks); setupTest(hooks);
test('respondForFindAll returns the correct data for list endpoint', function(assert) { test('respondForQuery returns the correct data for list endpoint', function(assert) {
const serializer = this.owner.lookup('serializer:dc'); const serializer = this.owner.lookup('serializer:dc');
const request = { const request = {
url: `/v1/catalog/datacenters`, url: `/v1/catalog/datacenters`,
}; };
return get(request.url).then(function(payload) { return get(request.url).then(function(payload) {
const expected = payload; const expected = payload;
const actual = serializer.respondForFindAll(function(cb) { const actual = serializer.respondForQuery(function(cb) {
const headers = {}; const headers = {};
const body = payload; const body = payload;
return cb(headers, body); return cb(headers, body);

View File

@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Controller | application', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.owner.lookup('controller:application');
assert.ok(controller);
});
});