ui: Add tab navigation to the browser history/URLs (#7592)
* ui: Add tab navigation to the browser history/URLs
This commit changes all our tabbed UI interfaces in the catalog to use
actual URL changes rather than only updating the content in the page
using CSS.
Originally we had decided not to add tab clicks into the browser
history for a variety of reasons. As the UI has progressed these tabs
are a fairly common pattern we are using and as the UI grows and
stabilizes around certain UX patterns we've decided to make these tabs
'URL changing'.
Pros:
- Deeplinking
- Potentially smaller Route files with a more concentrated scope of the
contents of a tab rather than the entire page.
- Tab clicks now go into your history meaning backwards and forwards
buttons take you through the tabs not just the pages.
- The majority of our partials are now fully fledged templates (Octane
🎉)
Cons:
- Tab clicks now go into your history meaning backwards and forwards
buttons take you through the tabs not just the pages. (Could be good and
bad from a UX perspective)
- Many more Route and Controller files (yet as mentioned above each of these
have a more reduced scope)
- Moving around the contents of these tabs, or changing the visual names
of them means updates to the URL structure, which then should
potentially entail redirects, therefore what things that seem like
straightforwards design reorganizations are now a little more impactful.
It was getting to the point that the Pros outweight the Cons
Apart from moving some files around we made a few more tiny tweaks to
get this all working:
- Our freetext-filter component now performs the initial search rather
than this happening in the Controller (remove of the search method in
the Controllers and the new didInsertElement hook in the component)
- All of the <TabNav>'s were changed to use its alternative href
approach.
- <TabPanel>s usage was mostly removed. This is th thing I dislike the
most. I think this needs removing, but I'd also like to remove the HTML
it creates. You'll see that every new page is wrappe din the HTML for
the old <TabPanel>, this is to continue to use the same HTML structure
and id's as before to avoid making further changes to any CSS that might
use this and being able to target things during testing. We could have
also removed these here, but it would have meant a much larger changeset
and can just as easily be done at a later date.
- We made a new `tabgroup` page-object component, which is almost
identical to the previous `radiogroup` one and injected that instead
where needed during testing.
* Make sure we pick up indexed routes when nspaces are enabled
* Move session invalidation to the child (session) route
* Revert back to not using didInsertElement for updating the searching
This adds a way for the searchable to remember the last search result
instead, which changes less and stick to the previous method of
searching.
This commit is contained in:
parent
8f60f3a3d1
commit
17f10ffd0d
|
@ -1,7 +1,10 @@
|
|||
<nav role="tablist" class="tab-nav">
|
||||
<ul>
|
||||
{{#each items as |item|}}
|
||||
<li class={{if (or item.selected (eq selected (if item.label (slugify item.label) (slugify item)))) 'selected'}}>
|
||||
<li
|
||||
data-test-tab={{concat name '_' (if item.label (slugify item.label) (slugify item))}}
|
||||
class={{if (or item.selected (eq selected (if item.label (slugify item.label) (slugify item)))) 'selected'}}
|
||||
>
|
||||
<label role="tab" onkeydown={{action 'keydown'}} tabindex="0" aria-controls="radiogroup_{{name}}_{{if item.label (slugify item.label) (slugify item)}}_panel" for="radiogroup_{{name}}_{{if item.label (slugify item.label) (slugify item)}}" data-test-radiobutton="{{name}}_{{if item.label (slugify item.label) (slugify item)}}">
|
||||
{{#if item.href }}
|
||||
<a href={{item.href}}>{{item.label}}</a>
|
||||
|
|
|
@ -1,26 +1,13 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get, set, computed } from '@ember/object';
|
||||
import { get } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import WithSearching from 'consul-ui/mixins/with-searching';
|
||||
import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source';
|
||||
|
||||
export default Controller.extend(WithEventSource, WithSearching, {
|
||||
export default Controller.extend(WithEventSource, {
|
||||
dom: service('dom'),
|
||||
notify: service('flashMessages'),
|
||||
items: alias('item.Services'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
init: function() {
|
||||
this.searchParams = {
|
||||
nodeservice: 's',
|
||||
};
|
||||
this._super(...arguments);
|
||||
},
|
||||
item: listen('item').catch(function(e) {
|
||||
if (e.target.readyState === 1) {
|
||||
// OPEN
|
||||
|
@ -36,33 +23,7 @@ export default Controller.extend(WithEventSource, WithSearching, {
|
|||
}
|
||||
}
|
||||
}),
|
||||
searchable: computed('items', function() {
|
||||
return get(this, 'searchables.nodeservice')
|
||||
.add(this.items)
|
||||
.search(get(this, this.searchParams.nodeservice));
|
||||
}),
|
||||
setProperties: function() {
|
||||
this._super(...arguments);
|
||||
// the default selected tab depends on whether you have any healthchecks or not
|
||||
// so check the length here.
|
||||
// This method is called immediately after `Route::setupController`, and done here rather than there
|
||||
// as this is a variable used purely for view level things, if the view was different we might not
|
||||
// need this variable
|
||||
set(this, 'selectedTab', get(this, 'item.Checks.length') > 0 ? 'health-checks' : 'services');
|
||||
},
|
||||
actions: {
|
||||
change: function(e) {
|
||||
set(this, 'selectedTab', e.target.value);
|
||||
// Ensure tabular-collections sizing is recalculated
|
||||
// now it is visible in the DOM
|
||||
this.dom
|
||||
.components('.tab-section input[type="radio"]:checked + div table')
|
||||
.forEach(function(item) {
|
||||
if (typeof item.didAppear === 'function') {
|
||||
item.didAppear();
|
||||
}
|
||||
});
|
||||
},
|
||||
sortChecksByImportance: function(a, b) {
|
||||
const statusA = get(a, 'Status');
|
||||
const statusB = get(b, 'Status');
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get, computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import WithSearching from 'consul-ui/mixins/with-searching';
|
||||
|
||||
export default Controller.extend(WithSearching, {
|
||||
dom: service('dom'),
|
||||
items: alias('item.Services'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
init: function() {
|
||||
this.searchParams = {
|
||||
nodeservice: 's',
|
||||
};
|
||||
this._super(...arguments);
|
||||
},
|
||||
searchable: computed('items', function() {
|
||||
return get(this, 'searchables.nodeservice')
|
||||
.add(this.items)
|
||||
.search(get(this, this.searchParams.nodeservice));
|
||||
}),
|
||||
});
|
|
@ -18,6 +18,6 @@ export default Controller.extend(WithEventSource, WithSearching, {
|
|||
searchable: computed('items.[]', function() {
|
||||
return get(this, 'searchables.nspace')
|
||||
.add(this.items)
|
||||
.search(this.terms);
|
||||
.search(get(this, this.searchParams.nspace));
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get, set } from '@ember/object';
|
||||
import { get } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source';
|
||||
|
||||
export default Controller.extend(WithEventSource, {
|
||||
notify: service('flashMessages'),
|
||||
setProperties: function() {
|
||||
this._super(...arguments);
|
||||
// This method is called immediately after `Route::setupController`, and done here rather than there
|
||||
// as this is a variable used purely for view level things, if the view was different we might not
|
||||
// need this variable
|
||||
set(this, 'selectedTab', 'service-checks');
|
||||
},
|
||||
item: listen('item').catch(function(e) {
|
||||
if (e.target.readyState === 1) {
|
||||
// OPEN
|
||||
|
@ -29,9 +22,4 @@ export default Controller.extend(WithEventSource, {
|
|||
}
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
change: function(e) {
|
||||
set(this, 'selectedTab', e.target.value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,27 +1,10 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get, set, computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { get } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithSearching from 'consul-ui/mixins/with-searching';
|
||||
import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source';
|
||||
export default Controller.extend(WithEventSource, WithSearching, {
|
||||
export default Controller.extend(WithEventSource, {
|
||||
dom: service('dom'),
|
||||
notify: service('flashMessages'),
|
||||
items: alias('item.Nodes'),
|
||||
init: function() {
|
||||
this.searchParams = {
|
||||
serviceInstance: 's',
|
||||
};
|
||||
this._super(...arguments);
|
||||
},
|
||||
setProperties: function() {
|
||||
this._super(...arguments);
|
||||
// This method is called immediately after `Route::setupController`, and done here rather than there
|
||||
// as this is a variable used purely for view level things, if the view was different we might not
|
||||
// need this variable
|
||||
|
||||
set(this, 'selectedTab', 'instances');
|
||||
},
|
||||
item: listen('item').catch(function(e) {
|
||||
if (e.target.readyState === 1) {
|
||||
// OPEN
|
||||
|
@ -35,23 +18,4 @@ export default Controller.extend(WithEventSource, WithSearching, {
|
|||
}
|
||||
}
|
||||
}),
|
||||
searchable: computed('items', function() {
|
||||
return get(this, 'searchables.serviceInstance')
|
||||
.add(this.items)
|
||||
.search(get(this, this.searchParams.serviceInstance));
|
||||
}),
|
||||
actions: {
|
||||
change: function(e) {
|
||||
set(this, 'selectedTab', e.target.value);
|
||||
// Ensure tabular-collections sizing is recalculated
|
||||
// now it is visible in the DOM
|
||||
this.dom
|
||||
.components('.tab-section input[type="radio"]:checked + div table')
|
||||
.forEach(function(item) {
|
||||
if (typeof item.didAppear === 'function') {
|
||||
item.didAppear();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get, computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithSearching from 'consul-ui/mixins/with-searching';
|
||||
export default Controller.extend(WithSearching, {
|
||||
dom: service('dom'),
|
||||
items: alias('item.Nodes'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
init: function() {
|
||||
this.searchParams = {
|
||||
serviceInstance: 's',
|
||||
};
|
||||
this._super(...arguments);
|
||||
},
|
||||
searchable: computed('items', function() {
|
||||
return get(this, 'searchables.serviceInstance')
|
||||
.add(this.items)
|
||||
.search(get(this, this.searchParams.serviceInstance));
|
||||
}),
|
||||
});
|
|
@ -29,6 +29,19 @@ Route.reopen(
|
|||
if (env('CONSUL_NSPACES_ENABLED')) {
|
||||
const dotRe = /\./g;
|
||||
initialize = function(container) {
|
||||
const register = function(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);
|
||||
}
|
||||
};
|
||||
const all = Object.keys(flat(routes))
|
||||
.filter(function(item) {
|
||||
return item.startsWith('dc');
|
||||
|
@ -38,21 +51,21 @@ if (env('CONSUL_NSPACES_ENABLED')) {
|
|||
});
|
||||
all.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 exists
|
||||
// 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(index, indexed);
|
||||
}
|
||||
route.reopen({
|
||||
templateName: item
|
||||
.replace('/root-create', '/create')
|
||||
.replace('/create', '/edit')
|
||||
.replace('/folder', '/index'),
|
||||
});
|
||||
container.register(`route:nspace/${item}`, route);
|
||||
const controller = container.resolveRegistration(`controller:${item}`);
|
||||
if (controller) {
|
||||
container.register(`controller:nspace/${item}`, controller);
|
||||
}
|
||||
register(route, item);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,8 +13,44 @@ export const routes = {
|
|||
// Show an individual service
|
||||
show: {
|
||||
_options: { path: '/:name' },
|
||||
instances: {
|
||||
_options: { path: '/instances' },
|
||||
},
|
||||
intentions: {
|
||||
_options: { path: '/intentions' },
|
||||
},
|
||||
routing: {
|
||||
_options: { path: '/routing' },
|
||||
},
|
||||
tags: {
|
||||
_options: { path: '/tags' },
|
||||
},
|
||||
},
|
||||
instance: {
|
||||
_options: { path: '/:name/instances/:node/:id' },
|
||||
servicechecks: {
|
||||
_options: { path: '/service-checks' },
|
||||
},
|
||||
nodechecks: {
|
||||
_options: { path: '/node-checks' },
|
||||
},
|
||||
upstreams: {
|
||||
_options: { path: '/upstreams' },
|
||||
},
|
||||
exposedpaths: {
|
||||
_options: { path: '/exposed-paths' },
|
||||
},
|
||||
addresses: {
|
||||
_options: { path: '/addresses' },
|
||||
},
|
||||
tags: {
|
||||
_options: { path: '/tags' },
|
||||
},
|
||||
metadata: {
|
||||
_options: { path: '/metadata' },
|
||||
},
|
||||
},
|
||||
notfound: {
|
||||
_options: { path: '/:name/:node/:id' },
|
||||
},
|
||||
},
|
||||
|
@ -24,6 +60,21 @@ export const routes = {
|
|||
// Show an individual node
|
||||
show: {
|
||||
_options: { path: '/:name' },
|
||||
healthchecks: {
|
||||
_options: { path: '/health-checks' },
|
||||
},
|
||||
services: {
|
||||
_options: { path: '/services' },
|
||||
},
|
||||
rtt: {
|
||||
_options: { path: '/round-trip-time' },
|
||||
},
|
||||
sessions: {
|
||||
_options: { path: '/lock-sessions' },
|
||||
},
|
||||
metadata: {
|
||||
_options: { path: '/meta-data' },
|
||||
},
|
||||
},
|
||||
},
|
||||
// Intentions represent a consul intention
|
||||
|
|
|
@ -2,18 +2,10 @@ import Route from '@ember/routing/route';
|
|||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
|
||||
export default Route.extend(WithBlockingActions, {
|
||||
export default Route.extend({
|
||||
repo: service('repository/node'),
|
||||
sessionRepo: service('repository/session'),
|
||||
coordinateRepo: service('repository/coordinate'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function(params) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||
|
@ -27,20 +19,4 @@ export default Route.extend(WithBlockingActions, {
|
|||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
actions: {
|
||||
invalidateSession: function(item) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||
const controller = this.controller;
|
||||
return this.feedback.execute(() => {
|
||||
return this.sessionRepo.remove(item).then(() => {
|
||||
return this.sessionRepo.findByNode(item.Node, dc, nspace).then(function(sessions) {
|
||||
controller.setProperties({
|
||||
sessions: sessions,
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 'delete');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
afterModel: function(model, transition) {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
// the default selected tab depends on whether you have any healthchecks or not
|
||||
// so check the length here.
|
||||
const to = get(model, 'item.Checks.length') > 0 ? 'healthchecks' : 'services';
|
||||
this.replaceWith(`${parent}.${to}`, model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
afterModel: function(model, transition) {
|
||||
if (!get(model, 'tomography.distances')) {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
this.replaceWith(parent);
|
||||
}
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
|
||||
export default Route.extend(WithBlockingActions, {
|
||||
sessionRepo: service('repository/session'),
|
||||
feedback: service('feedback'),
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
actions: {
|
||||
invalidateSession: function(item) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||
const controller = this.controller;
|
||||
return this.feedback.execute(() => {
|
||||
return this.sessionRepo.remove(item).then(() => {
|
||||
return this.sessionRepo.findByNode(item.Node, dc, nspace).then(function(sessions) {
|
||||
controller.setProperties({
|
||||
sessions: sessions,
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 'delete');
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
afterModel: function(model, transition) {
|
||||
if (get(model, 'item.Kind') !== 'mesh-gateway') {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
this.replaceWith(parent);
|
||||
}
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
afterModel: function(model, transition) {
|
||||
if (
|
||||
get(model, 'item.Kind') !== 'connect-proxy' ||
|
||||
get(model, 'item.Proxy.Expose.Paths.length') < 1
|
||||
) {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
this.replaceWith(parent);
|
||||
}
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import to from 'consul-ui/utils/routing/redirect-to';
|
||||
|
||||
export default Route.extend({
|
||||
redirect: to('servicechecks'),
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
afterModel: function(model, transition) {
|
||||
if (get(model, 'item.Kind') !== 'connect-proxy') {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
this.replaceWith(parent);
|
||||
}
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
redirect: function(model, transition) {
|
||||
this.replaceWith('dc.services.instance', model.name, model.node, model.id);
|
||||
},
|
||||
});
|
|
@ -7,12 +7,6 @@ export default Route.extend({
|
|||
repo: service('repository/service'),
|
||||
chainRepo: service('repository/discovery-chain'),
|
||||
settings: service('settings'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function(params) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import to from 'consul-ui/utils/routing/redirect-to';
|
||||
|
||||
export default Route.extend({
|
||||
redirect: to('instances'),
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
afterModel: function(model, transition) {
|
||||
if (!get(model, 'chain')) {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
this.replaceWith(parent);
|
||||
}
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
{{#if (gt item.Checks.length 0) }}
|
||||
<HealthcheckList @items={{item.Checks}} />
|
||||
{{else}}
|
||||
<p>
|
||||
This node has no health checks.
|
||||
</p>
|
||||
{{/if}}
|
|
@ -1,22 +0,0 @@
|
|||
<dl>
|
||||
<dt>
|
||||
Minimum
|
||||
</dt>
|
||||
<dd>
|
||||
{{format-number tomography.min maximumFractionDigits=2}}ms
|
||||
</dd>
|
||||
<dt>
|
||||
Median
|
||||
</dt>
|
||||
<dd>
|
||||
{{format-number tomography.median maximumFractionDigits=2}}ms
|
||||
</dd>
|
||||
<dt>
|
||||
Maximum
|
||||
</dt>
|
||||
<dd>
|
||||
{{format-number tomography.max maximumFractionDigits=2}}ms
|
||||
</dd>
|
||||
</dl>
|
||||
<TomographyGraph @tomography={{tomography}} />
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
{{#if (gt items.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<form class="filter-bar">
|
||||
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search by name/port" />
|
||||
</form>
|
||||
{{/if}}
|
||||
<ChangeableSet @dispatcher={{searchable}}>
|
||||
<BlockSlot @name="set" as |filtered|>
|
||||
<TabularCollection
|
||||
data-test-services
|
||||
@items={{filtered}} as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Service</th>
|
||||
<th>Port</th>
|
||||
<th>Tags</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td data-test-service-name={{item.Service}}>
|
||||
<a href={{href-to 'dc.services.show' item.Service}}>
|
||||
{{#let (service/external-source item) as |externalSource| }}
|
||||
{{#if externalSource }}
|
||||
<span data-test-external-source={{externalSource}} style={{concat 'background-image: var(--' externalSource '-icon)'}}></span>
|
||||
{{else}}
|
||||
<span></span>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{item.Service}}{{#if (not-eq item.ID item.Service) }} <em data-test-service-id="{{item.ID}}">({{item.ID}})</em>{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-service-port={{item.Port}} class="port">
|
||||
{{item.Port}}
|
||||
</td>
|
||||
<td data-test-service-tags>
|
||||
<TagList @items={{item.Tags}} />
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<p>
|
||||
There are no services.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
|
@ -14,7 +14,16 @@
|
|||
{{ item.Node }}
|
||||
</h1>
|
||||
<label for="toolbar-toggle"></label>
|
||||
<TabNav @items={{compact (array "Health Checks" "Services" (if tomography.distances "Round Trip Time" "") "Lock Sessions" "Meta Data")}} @selected={{selectedTab}} />
|
||||
<TabNav @items={{
|
||||
compact
|
||||
(array
|
||||
(hash label="Health Checks" href=(href-to "dc.nodes.show.healthchecks") selected=(is-href "dc.nodes.show.healthchecks"))
|
||||
(hash label="Services" href=(href-to "dc.nodes.show.services") selected=(is-href "dc.nodes.show.services"))
|
||||
(if tomography.distances (hash label="Round Trip Time" href=(href-to "dc.nodes.show.rtt") selected=(is-href "dc.nodes.show.rtt")) '')
|
||||
(hash label="Lock Sessions" href=(href-to "dc.nodes.show.sessions") selected=(is-href "dc.nodes.show.sessions"))
|
||||
(hash label="Meta Data" href=(href-to "dc.nodes.show.metadata") selected=(is-href "dc.nodes.show.metadata"))
|
||||
)
|
||||
}}/>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<FeedbackDialog @type="inline">
|
||||
|
@ -36,22 +45,6 @@
|
|||
</FeedbackDialog>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
{{#each
|
||||
(compact
|
||||
(array
|
||||
(hash id=(slugify 'Health Checks') partial='dc/nodes/healthchecks')
|
||||
(hash id=(slugify 'Services') partial='dc/nodes/services')
|
||||
(if tomography.distances (hash id=(slugify 'Round Trip Time') partial='dc/nodes/rtt') '')
|
||||
(hash id=(slugify 'Lock Sessions') partial='dc/nodes/sessions')
|
||||
(hash id=(slugify 'Meta Data') partial='dc/nodes/metadata')
|
||||
)
|
||||
) key="id" as |panel|
|
||||
}}
|
||||
{{#if (or (not-eq panel.id 'round-trip-time') (gt tomography.distances.length 0)) }}
|
||||
<TabSection @id={{panel.id}} @selected={{eq (if selectedTab selectedTab "") panel.id}} @onchange={{action "change"}}>
|
||||
{{partial panel.partial}}
|
||||
</TabSection>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{outlet}}
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<div id="health-checks" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt item.Checks.length 0) }}
|
||||
<HealthcheckList @items={{item.Checks}} />
|
||||
{{else}}
|
||||
<p>
|
||||
This node has no health checks.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -1,3 +1,5 @@
|
|||
<div id="metadata" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if item.Meta}}
|
||||
<ConsulMetadataList @items={{object-entries item.Meta}} />
|
||||
{{else}}
|
||||
|
@ -5,3 +7,5 @@
|
|||
This node has no meta data.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,26 @@
|
|||
<div id="round-trip-time" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
<dl>
|
||||
<dt>
|
||||
Minimum
|
||||
</dt>
|
||||
<dd>
|
||||
{{format-number tomography.min maximumFractionDigits=2}}ms
|
||||
</dd>
|
||||
<dt>
|
||||
Median
|
||||
</dt>
|
||||
<dd>
|
||||
{{format-number tomography.median maximumFractionDigits=2}}ms
|
||||
</dd>
|
||||
<dt>
|
||||
Maximum
|
||||
</dt>
|
||||
<dd>
|
||||
{{format-number tomography.max maximumFractionDigits=2}}ms
|
||||
</dd>
|
||||
</dl>
|
||||
<TomographyGraph @tomography={{tomography}} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<div id="services" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt items.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<form class="filter-bar">
|
||||
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search by name/port" />
|
||||
</form>
|
||||
{{/if}}
|
||||
<ChangeableSet @dispatcher={{searchable}}>
|
||||
<BlockSlot @name="set" as |filtered|>
|
||||
<TabularCollection
|
||||
data-test-services
|
||||
@items={{filtered}} as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Service</th>
|
||||
<th>Port</th>
|
||||
<th>Tags</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td data-test-service-name={{item.Service}}>
|
||||
<a href={{href-to 'dc.services.show' item.Service}}>
|
||||
{{#let (service/external-source item) as |externalSource| }}
|
||||
{{#if externalSource }}
|
||||
<span data-test-external-source={{externalSource}} style={{concat 'background-image: var(--' externalSource '-icon)'}}></span>
|
||||
{{else}}
|
||||
<span></span>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{item.Service}}{{#if (not-eq item.ID item.Service) }} <em data-test-service-id="{{item.ID}}">({{item.ID}})</em>{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-service-port={{item.Port}} class="port">
|
||||
{{item.Port}}
|
||||
</td>
|
||||
<td data-test-service-tags>
|
||||
<TagList @items={{item.Tags}} />
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<p>
|
||||
There are no services.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
</div>
|
||||
</div>
|
|
@ -1,3 +1,5 @@
|
|||
<div id="lock-sessions" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt sessions.length 0)}}
|
||||
<TabularCollection
|
||||
data-test-sessions
|
||||
|
@ -55,4 +57,5 @@
|
|||
There are no Lock Sessions for this Node. For more information, view <a href="{{ env 'CONSUL_DOCS_URL'}}/internals/sessions.html" rel="help noopener noreferrer" target="_blank">our documentation</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
{{#if item.TaggedAddresses }}
|
||||
<TabularCollection
|
||||
data-test-addresses
|
||||
@items={{object-entries item.TaggedAddresses}} as |taggedAddress index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Tag</th>
|
||||
<th>Address</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
{{#with (object-at 1 taggedAddress) as |address|}}
|
||||
<td>
|
||||
{{object-at 0 taggedAddress}}{{#if (and (eq address.Address item.Address) (eq address.Port item.Port))}} <em data-test-address-default>(default)</em>{{/if}}
|
||||
</td>
|
||||
<td data-test-address>
|
||||
{{address.Address}}:{{address.Port}}
|
||||
</td>
|
||||
{{/with}}
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
{{else}}
|
||||
<p>
|
||||
There are no additional addresses.
|
||||
</p>
|
||||
{{/if}}
|
|
@ -1,33 +0,0 @@
|
|||
<p>
|
||||
You can expose individual HTTP paths like /metrics through Envoy for external services like Prometheus.
|
||||
</p>
|
||||
<TabularCollection
|
||||
data-test-exposedpaths
|
||||
class="exposedpaths"
|
||||
@items={{item.Proxy.Expose.Paths}} as |path index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Path</th>
|
||||
<th>Protocol</th>
|
||||
<th>Listener port</th>
|
||||
<th>Local path port</th>
|
||||
<th>Combined address<span><em role="tooltip">Service address, listener port, and path all combined into one URL.</em></span></th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td>
|
||||
<span>{{path.Path}}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{path.Protocol}}
|
||||
</td>
|
||||
<td>
|
||||
{{path.ListenerPort}}
|
||||
</td>
|
||||
<td>
|
||||
{{path.LocalPathPort}}
|
||||
</td>
|
||||
<td>
|
||||
<span data-test-combined-address>{{item.Address}}:{{path.ListenerPort}}{{path.Path}}</span>
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
|
@ -1,57 +0,0 @@
|
|||
{{#if (gt items.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<form class="filter-bar">
|
||||
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
|
||||
</form>
|
||||
{{/if}}
|
||||
<ChangeableSet @dispatcher={{searchable}}>
|
||||
<BlockSlot @name="set" as |filtered|>
|
||||
<TabularCollection
|
||||
data-test-instances
|
||||
@items={{filtered}} as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>ID</th>
|
||||
<th>Node</th>
|
||||
<th>Address</th>
|
||||
<th>Node Checks</th>
|
||||
<th>Service Checks</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td data-test-id={{item.Service.ID}}>
|
||||
<a href={{href-to 'dc.services.instance' item.Service.Service item.Node.Node (or item.Service.ID item.Service.Service)}}>
|
||||
{{#let (service/external-source item.Service) as |externalSource| }}
|
||||
{{#if externalSource }}
|
||||
<span data-test-external-source={{externalSource}} style={{concat 'background-image: var(--' externalSource '-icon)'}}></span>
|
||||
{{else}}
|
||||
<span></span>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{or item.Service.ID item.Service.Service}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-node>
|
||||
<a href={{href-to 'dc.nodes.show' item.Node.Node}}>{{item.Node.Node}}</a>
|
||||
</td>
|
||||
<td data-test-address>
|
||||
{{item.Service.Address}}:{{item.Service.Port}}
|
||||
</td>
|
||||
<td>
|
||||
{{#with (reject-by 'ServiceID' '' item.Checks) as |checks|}}
|
||||
<HealthcheckInfo @passing={{filter-by "Status" "passing" checks}} @warning={{filter-by "Status" "warning" checks}} @critical={{filter-by "Status" "critical" checks}} />
|
||||
{{/with}}
|
||||
</td>
|
||||
<td>
|
||||
{{#with (filter-by 'ServiceID' '' item.Checks) as |checks|}}
|
||||
<HealthcheckInfo @passing={{filter-by "Status" "passing" checks}} @warning={{filter-by "Status" "warning" checks}} @critical={{filter-by "Status" "critical" checks}} />
|
||||
{{/with}}
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<p>
|
||||
There are no services.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
|
@ -1,8 +0,0 @@
|
|||
{{#if (gt item.NodeChecks.length 0) }}
|
||||
<HealthcheckList @items={{item.NodeChecks}} />
|
||||
{{else}}
|
||||
<p>
|
||||
This instance has no node health checks.
|
||||
</p>
|
||||
{{/if}}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<DiscoveryChain @chain={{chain.Chain}} />
|
|
@ -1,8 +0,0 @@
|
|||
{{#if (gt item.ServiceChecks.length 0) }}
|
||||
<HealthcheckList @items={{item.ServiceChecks}} @exposed={{proxy.ServiceProxy.Expose.Checks}} />
|
||||
{{else}}
|
||||
<p>
|
||||
This instance has no service health checks.
|
||||
</p>
|
||||
{{/if}}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{{#if (gt item.Tags.length 0) }}
|
||||
<TagList @items={{item.Tags}} />
|
||||
{{else}}
|
||||
<p>
|
||||
There are no tags.
|
||||
</p>
|
||||
{{/if}}
|
|
@ -1,39 +0,0 @@
|
|||
{{#if (gt item.Proxy.Upstreams.length 0) }}
|
||||
<TabularCollection
|
||||
data-test-upstreams
|
||||
@items={{item.Proxy.Upstreams}} as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Upstream</th>
|
||||
<th>Datacenter</th>
|
||||
<th>Type</th>
|
||||
<th>Local Bind Address</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td>
|
||||
<a data-test-destination-name>
|
||||
{{item.DestinationName}}
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
{{#if (not-eq item.DestinationType 'prepared_query')}}
|
||||
{{! TODO: slugify }}
|
||||
<em class={{concat 'nspace-' (or item.DestinationNamespace 'default')}}>{{or item.DestinationNamespace 'default'}}</em>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-destination-datacenter>
|
||||
{{item.Datacenter}}
|
||||
</td>
|
||||
<td data-test-destination-type>
|
||||
{{item.DestinationType}}
|
||||
</td>
|
||||
<td data-test-local-bind-address>
|
||||
{{item.LocalBindAddress}}:{{item.LocalBindPort}}
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
{{else}}
|
||||
<p>
|
||||
There are no upstreams.
|
||||
</p>
|
||||
{{/if}}
|
|
@ -62,32 +62,27 @@
|
|||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
<TabNav @items={{compact (array "Service Checks" "Node Checks" (if (eq item.Kind "connect-proxy") "Upstreams" "") (if (and (eq item.Kind "connect-proxy") (gt item.Proxy.Expose.Paths.length 0)) "Exposed Paths" "") (if (eq item.Kind "mesh-gateway") "Addresses" "") "Tags" "Meta Data")}} @selected={{selectedTab}} />
|
||||
{{#each
|
||||
(compact
|
||||
<TabNav @items={{
|
||||
compact
|
||||
(array
|
||||
(hash id=(slugify 'Service Checks') partial='dc/services/servicechecks')
|
||||
(hash id=(slugify 'Node Checks') partial='dc/services/nodechecks')
|
||||
(hash label="Service Checks" href=(href-to "dc.services.instance.servicechecks") selected=(is-href "dc.services.instance.servicechecks"))
|
||||
(hash label="Node Checks" href=(href-to "dc.services.instance.nodechecks") selected=(is-href "dc.services.instance.nodechecks"))
|
||||
(if
|
||||
(eq item.Kind 'connect-proxy')
|
||||
(hash id=(slugify 'Upstreams') partial='dc/services/upstreams') ''
|
||||
(hash label="Upstreams" href=(href-to "dc.services.instance.upstreams") selected=(is-href "dc.services.instance.upstreams")) ""
|
||||
)
|
||||
(if
|
||||
(and (eq item.Kind 'connect-proxy') (gt item.Proxy.Expose.Paths.length 0))
|
||||
(hash id=(slugify 'Exposed Paths') partial='dc/services/exposedpaths') ''
|
||||
(hash label="Exposed Paths" href=(href-to "dc.services.instance.exposedpaths") selected=(is-href "dc.services.instance.exposedpaths")) ""
|
||||
)
|
||||
(if
|
||||
(eq item.Kind 'mesh-gateway')
|
||||
(hash id=(slugify 'Addresses') partial='dc/services/addresses') ''
|
||||
(hash label="Addresses" href=(href-to "dc.services.instance.addresses") selected=(is-href "dc.services.instance.addresses")) ""
|
||||
)
|
||||
(hash id=(slugify 'Tags') partial='dc/services/tags')
|
||||
(hash id=(slugify 'Meta Data') partial='dc/services/metadata')
|
||||
(hash label="Tags" href=(href-to "dc.services.instance.tags") selected=(is-href "dc.services.instance.tags"))
|
||||
(hash label="Meta Data" href=(href-to "dc.services.instance.metadata") selected=(is-href "dc.services.instance.metadata"))
|
||||
)
|
||||
) key="id" as |panel|
|
||||
}}
|
||||
<TabSection @id={{panel.id}} @selected={{eq (if selectedTab selectedTab "") panel.id}} @onchange={{action "change"}}>
|
||||
{{partial panel.partial}}
|
||||
</TabSection>
|
||||
{{/each}}
|
||||
}}/>
|
||||
{{outlet}}
|
||||
</BlockSlot>
|
||||
</AppView>
|
|
@ -0,0 +1,29 @@
|
|||
<div id="addresses" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if item.TaggedAddresses }}
|
||||
<TabularCollection
|
||||
data-test-addresses
|
||||
@items={{object-entries item.TaggedAddresses}} as |taggedAddress index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Tag</th>
|
||||
<th>Address</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
{{#with (object-at 1 taggedAddress) as |address|}}
|
||||
<td>
|
||||
{{object-at 0 taggedAddress}}{{#if (and (eq address.Address item.Address) (eq address.Port item.Port))}} <em data-test-address-default>(default)</em>{{/if}}
|
||||
</td>
|
||||
<td data-test-address>
|
||||
{{address.Address}}:{{address.Port}}
|
||||
</td>
|
||||
{{/with}}
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
{{else}}
|
||||
<p>
|
||||
There are no additional addresses.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,37 @@
|
|||
<div id="exposed-paths" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
<p>
|
||||
You can expose individual HTTP paths like /metrics through Envoy for external services like Prometheus.
|
||||
</p>
|
||||
<TabularCollection
|
||||
data-test-exposedpaths
|
||||
class="exposedpaths"
|
||||
@items={{item.Proxy.Expose.Paths}} as |path index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Path</th>
|
||||
<th>Protocol</th>
|
||||
<th>Listener port</th>
|
||||
<th>Local path port</th>
|
||||
<th>Combined address<span><em role="tooltip">Service address, listener port, and path all combined into one URL.</em></span></th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td>
|
||||
<span>{{path.Path}}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{path.Protocol}}
|
||||
</td>
|
||||
<td>
|
||||
{{path.ListenerPort}}
|
||||
</td>
|
||||
<td>
|
||||
{{path.LocalPathPort}}
|
||||
</td>
|
||||
<td>
|
||||
<span data-test-combined-address>{{item.Address}}:{{path.ListenerPort}}{{path.Path}}</span>
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,32 @@
|
|||
<div id="meta-data" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if item.Meta}}
|
||||
{{#with (object-entries item.Meta) as |meta|}}
|
||||
<TabularCollection
|
||||
data-test-metadata
|
||||
@items={{meta}} as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td>
|
||||
<span>
|
||||
{{object-at 0 item}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{object-at 1 item}}</span>
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
{{/with}}
|
||||
{{else}}
|
||||
<p>
|
||||
This instance has no meta data.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<div id="node-checks" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt item.NodeChecks.length 0) }}
|
||||
<HealthcheckList @items={{item.NodeChecks}} />
|
||||
{{else}}
|
||||
<p>
|
||||
This instance has no node health checks.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<div id="service-checks" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt item.ServiceChecks.length 0) }}
|
||||
<HealthcheckList @items={{item.ServiceChecks}} @exposed={{proxy.ServiceProxy.Expose.Checks}} />
|
||||
{{else}}
|
||||
<p>
|
||||
This instance has no service health checks.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
<div id="tags" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt item.Tags.length 0) }}
|
||||
<TagList @items={{item.Tags}} />
|
||||
{{else}}
|
||||
<p>
|
||||
There are no tags.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,43 @@
|
|||
<div id="upstreams" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt item.Proxy.Upstreams.length 0) }}
|
||||
<TabularCollection
|
||||
data-test-upstreams
|
||||
@items={{item.Proxy.Upstreams}} as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>Upstream</th>
|
||||
<th>Datacenter</th>
|
||||
<th>Type</th>
|
||||
<th>Local Bind Address</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td>
|
||||
<a data-test-destination-name>
|
||||
{{item.DestinationName}}
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
{{#if (not-eq item.DestinationType 'prepared_query')}}
|
||||
{{! TODO: slugify }}
|
||||
<em class={{concat 'nspace-' (or item.DestinationNamespace 'default')}}>{{or item.DestinationNamespace 'default'}}</em>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-destination-datacenter>
|
||||
{{item.Datacenter}}
|
||||
</td>
|
||||
<td data-test-destination-type>
|
||||
{{item.DestinationType}}
|
||||
</td>
|
||||
<td data-test-local-bind-address>
|
||||
{{item.LocalBindAddress}}:{{item.LocalBindPort}}
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
{{else}}
|
||||
<p>
|
||||
There are no upstreams.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -23,7 +23,14 @@
|
|||
{{/if}}
|
||||
</h1>
|
||||
<label for="toolbar-toggle"></label>
|
||||
<TabNav @items={{compact (array "Instances" (if (not-eq chain ) "Routing" "") "Tags")}} @selected={{selectedTab}} />
|
||||
<TabNav @items={{
|
||||
compact
|
||||
(array
|
||||
(hash label="Instances" href=(href-to "dc.services.show.instances") selected=(is-href "dc.services.show.instances"))
|
||||
(if (not-eq chain) (hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing")) '')
|
||||
(hash label="Tags" href=(href-to "dc.services.show.tags") selected=(is-href "dc.services.show.tags"))
|
||||
)
|
||||
}}/>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
{{#if urls.service}}
|
||||
|
@ -31,18 +38,6 @@
|
|||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
{{#each
|
||||
(compact
|
||||
(array
|
||||
(hash id=(slugify 'Instances') partial='dc/services/instances')
|
||||
(if (not-eq chain null) (hash id=(slugify 'Routing') partial='dc/services/routing') '')
|
||||
(hash id=(slugify 'Tags') partial='dc/services/tags')
|
||||
)
|
||||
) key="id" as |panel|
|
||||
}}
|
||||
<TabSection @id={{panel.id}} @selected={{eq (if selectedTab selectedTab "") panel.id}} @onchange={{action "change"}}>
|
||||
{{partial panel.partial}}
|
||||
</TabSection>
|
||||
{{/each}}
|
||||
{{outlet}}
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<div id="instances" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt items.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<form class="filter-bar">
|
||||
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
|
||||
</form>
|
||||
{{/if}}
|
||||
<ChangeableSet @dispatcher={{searchable}}>
|
||||
<BlockSlot @name="set" as |filtered|>
|
||||
<TabularCollection
|
||||
data-test-instances
|
||||
@items={{filtered}} as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<th>ID</th>
|
||||
<th>Node</th>
|
||||
<th>Address</th>
|
||||
<th>Node Checks</th>
|
||||
<th>Service Checks</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td data-test-id={{item.Service.ID}}>
|
||||
<a href={{href-to 'dc.services.instance' item.Service.Service item.Node.Node (or item.Service.ID item.Service.Service)}}>
|
||||
{{#let (service/external-source item.Service) as |externalSource| }}
|
||||
{{#if externalSource }}
|
||||
<span data-test-external-source={{externalSource}} style={{concat 'background-image: var(--' externalSource '-icon)'}}></span>
|
||||
{{else}}
|
||||
<span></span>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{or item.Service.ID item.Service.Service}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-node>
|
||||
<a href={{href-to 'dc.nodes.show' item.Node.Node}}>{{item.Node.Node}}</a>
|
||||
</td>
|
||||
<td data-test-address>
|
||||
{{item.Service.Address}}:{{item.Service.Port}}
|
||||
</td>
|
||||
<td>
|
||||
{{#with (reject-by 'ServiceID' '' item.Checks) as |checks|}}
|
||||
<HealthcheckInfo @passing={{filter-by "Status" "passing" checks}} @warning={{filter-by "Status" "warning" checks}} @critical={{filter-by "Status" "critical" checks}} />
|
||||
{{/with}}
|
||||
</td>
|
||||
<td>
|
||||
{{#with (filter-by 'ServiceID' '' item.Checks) as |checks|}}
|
||||
<HealthcheckInfo @passing={{filter-by "Status" "passing" checks}} @warning={{filter-by "Status" "warning" checks}} @critical={{filter-by "Status" "critical" checks}} />
|
||||
{{/with}}
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<p>
|
||||
There are no services.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div id="routing" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
<DiscoveryChain @chain={{chain.Chain}} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<div id="tags" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt item.Tags.length 0) }}
|
||||
<TagList @items={{item.Tags}} />
|
||||
{{else}}
|
||||
<p>
|
||||
There are no tags.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
export default function(to, route) {
|
||||
return function(model, transition) {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
this.replaceWith(`${parent}.${to}`, model);
|
||||
};
|
||||
}
|
|
@ -4,8 +4,9 @@ export default function(EventTarget = RSVP.EventTarget, P = Promise) {
|
|||
return function(filter) {
|
||||
return EventTarget.mixin({
|
||||
value: '',
|
||||
_data: [],
|
||||
add: function(data) {
|
||||
this.data = data;
|
||||
this.data = this._data = data;
|
||||
return this;
|
||||
},
|
||||
find: function(terms = []) {
|
||||
|
@ -21,7 +22,7 @@ export default function(EventTarget = RSVP.EventTarget, P = Promise) {
|
|||
return prev.filter(item => {
|
||||
return filter(item, { s: term });
|
||||
});
|
||||
}, this.data)
|
||||
}, this._data)
|
||||
);
|
||||
},
|
||||
search: function(terms = []) {
|
||||
|
@ -30,6 +31,7 @@ export default function(EventTarget = RSVP.EventTarget, P = Promise) {
|
|||
// flow now for later on
|
||||
this.find(Array.isArray(terms) ? terms : [terms]).then(data => {
|
||||
// TODO: For the moment, lets just fake a target
|
||||
this.data = data;
|
||||
this.trigger('change', {
|
||||
target: {
|
||||
value: this.value.join('\n'),
|
||||
|
|
|
@ -20,6 +20,6 @@ Feature: components / copy-button
|
|||
dc: dc-1
|
||||
node: node-0
|
||||
---
|
||||
Then the url should be /dc-1/nodes/node-0
|
||||
Then the url should be /dc-1/nodes/node-0/health-checks
|
||||
When I click ".healthcheck-output:nth-child(1) button.copy-btn"
|
||||
Then I see the text "Copied output!" in ".healthcheck-output:nth-child(1) p.feedback-dialog-out"
|
||||
|
|
|
@ -44,7 +44,7 @@ Feature: dc / list-blocking
|
|||
And an external edit results in 0 [Model] models
|
||||
And pause until I see the text "deregistered" in "[data-notification]"
|
||||
Where:
|
||||
-------------------------------------------------------
|
||||
-----------------------------------------------------------------
|
||||
| Page | Model | Url |
|
||||
| service | instance | services/service-0-proxy |
|
||||
-------------------------------------------------------
|
||||
| service | instance | services/service-0-proxy/instances |
|
||||
-----------------------------------------------------------------
|
||||
|
|
|
@ -19,20 +19,20 @@ Feature: dc / nodes / sessions / invalidate: Invalidate Lock Sessions
|
|||
dc: dc1
|
||||
node: node-0
|
||||
---
|
||||
Then the url should be /dc1/nodes/node-0
|
||||
Then the url should be /dc1/nodes/node-0/health-checks
|
||||
And I click lockSessions on the tabs
|
||||
Then I see lockSessionsIsSelected on the tabs
|
||||
Scenario: Invalidating the lock session
|
||||
And I click delete on the sessions
|
||||
And I click confirmDelete on the sessions
|
||||
Then a PUT request was made to "/v1/session/destroy/7bbbd8bb-fff3-4292-b6e3-cfedd788546a?dc=dc1&ns=@!namespace"
|
||||
Then the url should be /dc1/nodes/node-0
|
||||
Then the url should be /dc1/nodes/node-0/lock-sessions
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "success" class
|
||||
Scenario: Invalidating a lock session and receiving an error
|
||||
Given the url "/v1/session/destroy/7bbbd8bb-fff3-4292-b6e3-cfedd788546a?dc=dc1&ns=@!namespace" responds with a 500 status
|
||||
And I click delete on the sessions
|
||||
And I click confirmDelete on the sessions
|
||||
Then the url should be /dc1/nodes/node-0
|
||||
Then the url should be /dc1/nodes/node-0/lock-sessions
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "error" class
|
||||
|
|
|
@ -74,7 +74,7 @@ Feature: dc / nodes / show: Show node
|
|||
dc: dc1
|
||||
node: node-0
|
||||
---
|
||||
Then the url should be /dc1/nodes/node-0
|
||||
Then the url should be /dc1/nodes/node-0/health-checks
|
||||
And the title should be "node-0 - Consul"
|
||||
And the url "/v1/internal/ui/node/node-0" responds with a 404 status
|
||||
And pause until I see the text "no longer exists" in "[data-notification]"
|
||||
|
|
|
@ -10,7 +10,7 @@ Feature: dc / services / instances / error: Visit Service Instance what doesn't
|
|||
node: node-0
|
||||
id: id-that-doesnt-exist
|
||||
---
|
||||
Then the url should be /dc1/services/service-0/node-0/id-that-doesnt-exist
|
||||
Then the url should be /dc1/services/service-0/instances/node-0/id-that-doesnt-exist
|
||||
And I see the text "404 (Unable to find instance)" in "[data-test-error]"
|
||||
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ Feature: dc / services / instances / gateway: Show Gateway Service Instance
|
|||
node: node-0
|
||||
id: gateway-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/gateway/node-0/gateway-with-id
|
||||
Then the url should be /dc1/services/gateway/instances/node-0/gateway-with-id/service-checks
|
||||
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ Feature: dc / services / instances / proxy: Show Proxy Service Instance
|
|||
node: node-0
|
||||
id: service-0-proxy-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0-proxy/node-0/service-0-proxy-with-id
|
||||
Then the url should be /dc1/services/service-0-proxy/instances/node-0/service-0-proxy-with-id/service-checks
|
||||
And I see destination on the proxy like "service"
|
||||
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
|
@ -125,7 +125,7 @@ Feature: dc / services / instances / proxy: Show Proxy Service Instance
|
|||
node: node-0
|
||||
id: service-0-proxy-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0-proxy/node-0/service-0-proxy-with-id
|
||||
Then the url should be /dc1/services/service-0-proxy/instances/node-0/service-0-proxy-with-id/service-checks
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
|
||||
When I click serviceChecks on the tabs
|
||||
|
@ -164,7 +164,7 @@ Feature: dc / services / instances / proxy: Show Proxy Service Instance
|
|||
node: node-0
|
||||
id: service-0-proxy-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0-proxy/node-0/service-0-proxy-with-id
|
||||
Then the url should be /dc1/services/service-0-proxy/instances/node-0/service-0-proxy-with-id/service-checks
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
|
||||
And I don't see exposedPaths on the tabs
|
||||
|
|
|
@ -60,7 +60,7 @@ Feature: dc / services / instances / show: Show Service Instance
|
|||
node: another-node
|
||||
id: service-0-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0/another-node/service-0-with-id
|
||||
Then the url should be /dc1/services/service-0/instances/another-node/service-0-with-id/service-checks
|
||||
Then I don't see type on the proxy
|
||||
Then I see externalSource like "nomad"
|
||||
|
||||
|
@ -98,7 +98,7 @@ Feature: dc / services / instances / show: Show Service Instance
|
|||
node: node-0
|
||||
id: service-0-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0/node-0/service-0-with-id
|
||||
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/service-checks
|
||||
And an external edit results in 0 instance models
|
||||
And pause until I see the text "deregistered" in "[data-notification]"
|
||||
|
||||
|
@ -119,7 +119,7 @@ Feature: dc / services / instances / show: Show Service Instance
|
|||
node: another-node
|
||||
id: service-0-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0/another-node/service-0-with-id
|
||||
Then the url should be /dc1/services/service-0/instances/another-node/service-0-with-id/service-checks
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
|
||||
And I don't see exposedPaths on the tabs
|
||||
|
@ -147,7 +147,7 @@ Feature: dc / services / instances / show: Show Service Instance
|
|||
node: another-node
|
||||
id: service-0-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0/another-node/service-0-with-id
|
||||
Then the url should be /dc1/services/service-0/instances/another-node/service-0-with-id/service-checks
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
|
||||
And I don't see exposedPaths on the tabs
|
||||
|
|
|
@ -19,7 +19,7 @@ Feature: dc / services / instances / sidecar-proxy: Show Sidecar Proxy Service I
|
|||
node: node-0
|
||||
id: service-0-sidecar-proxy-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0-sidecar-proxy/node-0/service-0-sidecar-proxy-with-id
|
||||
Then the url should be /dc1/services/service-0-sidecar-proxy/instances/node-0/service-0-sidecar-proxy-with-id/service-checks
|
||||
And I see destination on the proxy like "instance"
|
||||
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
|
|
|
@ -14,7 +14,7 @@ Feature: dc / services / instances / with-proxy: Show Service Instance with a pr
|
|||
node: node-0
|
||||
id: service-0-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0/node-0/service-0-with-id
|
||||
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/service-checks
|
||||
And I see type on the proxy like "proxy"
|
||||
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
|
|
|
@ -16,7 +16,7 @@ Feature: dc / services / instances / with-sidecar: Show Service Instance with a
|
|||
node: node-0
|
||||
id: service-0-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0/node-0/service-0-with-id
|
||||
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/service-checks
|
||||
And I see type on the proxy like "sidecar-proxy"
|
||||
And I see serviceChecksIsSelected on the tabs
|
||||
And I don't see upstreams on the tabs
|
||||
|
@ -36,7 +36,7 @@ Feature: dc / services / instances / with-sidecar: Show Service Instance with a
|
|||
node: node-0
|
||||
id: service-0-with-id
|
||||
---
|
||||
Then the url should be /dc1/services/service-0/node-0/service-0-with-id
|
||||
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/service-checks
|
||||
Then I don't see type on the proxy
|
||||
|
||||
|
||||
|
|
|
@ -17,5 +17,5 @@ Feature: dc / services / show-with-slashes: Show Service that has slashes in its
|
|||
Then the url should be /dc1/services
|
||||
Then I see 1 service model
|
||||
And I click service on the services
|
||||
Then the url should be /dc1/services/hashicorp%2Fservice%2Fservice-0
|
||||
Then the url should be /dc1/services/hashicorp%2Fservice%2Fservice-0/instances
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ Feature: dc / services / show: Show Service
|
|||
dc: dc1
|
||||
service: service-0
|
||||
---
|
||||
And I click tags on the tabs
|
||||
Then I see the text "Tag1" in "[data-test-tags] span:nth-child(1)"
|
||||
Then I see the text "Tag2" in "[data-test-tags] span:nth-child(2)"
|
||||
Then I see the text "Tag3" in "[data-test-tags] span:nth-child(3)"
|
||||
|
|
|
@ -42,8 +42,8 @@ Feature: page-navigation
|
|||
Where:
|
||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
| Item | Model | URL | Endpoint | Back |
|
||||
| service | services | /dc-1/services/service-0 | /v1/discovery-chain/service-0?dc=dc-1&ns=@namespace | /dc-1/services |
|
||||
| node | nodes | /dc-1/nodes/node-0 | /v1/session/node/node-0?dc=dc-1&ns=@namespace | /dc-1/nodes |
|
||||
| service | services | /dc-1/services/service-0/instances | /v1/discovery-chain/service-0?dc=dc-1&ns=@namespace | /dc-1/services |
|
||||
| node | nodes | /dc-1/nodes/node-0/health-checks | /v1/session/node/node-0?dc=dc-1&ns=@namespace | /dc-1/nodes |
|
||||
| kv | kvs | /dc-1/kv/0-key-value/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1&ns=@namespace | /dc-1/kv |
|
||||
# | acl | acls | /dc-1/acls/anonymous | /v1/acl/info/anonymous?dc=dc-1 | /dc-1/acls |
|
||||
| intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/internal/ui/services?dc=dc-1&ns=* | /dc-1/intentions |
|
||||
|
@ -59,7 +59,7 @@ Feature: page-navigation
|
|||
dc: dc-1
|
||||
node: node-0
|
||||
---
|
||||
Then the url should be /dc-1/nodes/node-0
|
||||
Then the url should be /dc-1/nodes/node-0/health-checks
|
||||
Then the last GET requests included from yaml
|
||||
---
|
||||
- /v1/catalog/datacenters
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { is, clickable } from 'ember-cli-page-object';
|
||||
import ucfirst from 'consul-ui/utils/ucfirst';
|
||||
export default function(name, items, blankKey = 'all') {
|
||||
return items.reduce(function(prev, item, i, arr) {
|
||||
// if item is empty then it means 'all'
|
||||
// otherwise camelCase based on something-here = somethingHere for the key
|
||||
const key =
|
||||
item === ''
|
||||
? blankKey
|
||||
: item.split('-').reduce(function(prev, item, i, arr) {
|
||||
if (i === 0) {
|
||||
return item;
|
||||
}
|
||||
return prev + ucfirst(item);
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
...{
|
||||
[`${key}IsSelected`]: is('.selected', `[data-test-tab="${name}_${item}"]`),
|
||||
[key]: clickable(`[data-test-tab="${name}_${item}"] > label > a`),
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
}
|
|
@ -16,6 +16,7 @@ import createCancelable from 'consul-ui/tests/lib/page-object/createCancelable';
|
|||
|
||||
import page from 'consul-ui/tests/pages/components/page';
|
||||
import radiogroup from 'consul-ui/tests/lib/page-object/radiogroup';
|
||||
import tabgroup from 'consul-ui/tests/lib/page-object/tabgroup';
|
||||
import freetextFilter from 'consul-ui/tests/pages/components/freetext-filter';
|
||||
import catalogFilter from 'consul-ui/tests/pages/components/catalog-filter';
|
||||
import aclFilter from 'consul-ui/tests/pages/components/acl-filter';
|
||||
|
@ -70,10 +71,10 @@ export default {
|
|||
services: create(
|
||||
services(visitable, clickable, attribute, collection, page, catalogFilter, radiogroup)
|
||||
),
|
||||
service: create(service(visitable, attribute, collection, text, catalogFilter, radiogroup)),
|
||||
instance: create(instance(visitable, attribute, collection, text, radiogroup)),
|
||||
service: create(service(visitable, attribute, collection, text, catalogFilter, tabgroup)),
|
||||
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
|
||||
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
|
||||
node: create(node(visitable, deletable, clickable, attribute, collection, radiogroup)),
|
||||
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup)),
|
||||
kvs: create(kvs(visitable, deletable, creatable, clickable, attribute, collection)),
|
||||
kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)),
|
||||
acls: create(acls(visitable, deletable, creatable, clickable, attribute, collection, aclFilter)),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export default function(visitable, deletable, clickable, attribute, collection, radiogroup) {
|
||||
export default function(visitable, deletable, clickable, attribute, collection, tabs) {
|
||||
return {
|
||||
visit: visitable('/:dc/nodes/:node'),
|
||||
tabs: radiogroup('tab', [
|
||||
tabs: tabs('tab', [
|
||||
'health-checks',
|
||||
'services',
|
||||
'round-trip-time',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export default function(visitable, attribute, collection, text, radiogroup) {
|
||||
export default function(visitable, attribute, collection, text, tabs) {
|
||||
return {
|
||||
visit: visitable('/:dc/services/:service/:node/:id'),
|
||||
visit: visitable('/:dc/services/:service/instances/:node/:id'),
|
||||
externalSource: attribute('data-test-external-source', 'h1 span'),
|
||||
tabs: radiogroup('tab', [
|
||||
tabs: tabs('tab', [
|
||||
'service-checks',
|
||||
'node-checks',
|
||||
'addresses',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default function(visitable, attribute, collection, text, filter, radiogroup) {
|
||||
export default function(visitable, attribute, collection, text, filter, tabs) {
|
||||
return {
|
||||
visit: visitable('/:dc/services/:service'),
|
||||
externalSource: attribute('data-test-external-source', 'h1 span'),
|
||||
|
@ -8,7 +8,7 @@ export default function(visitable, attribute, collection, text, filter, radiogro
|
|||
dashboardAnchor: {
|
||||
href: attribute('href', '[data-test-dashboard-anchor]'),
|
||||
},
|
||||
tabs: radiogroup('tab', ['instances', 'routing', 'tags']),
|
||||
tabs: tabs('tab', ['instances', 'routing', 'tags']),
|
||||
filter: filter,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue