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:
John Cowen 2020-04-08 10:56:36 +01:00 committed by John Cowen
parent 8f60f3a3d1
commit 17f10ffd0d
81 changed files with 931 additions and 477 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
{{#if (gt item.Checks.length 0) }}
<HealthcheckList @items={{item.Checks}} />
{{else}}
<p>
This node has no health checks.
</p>
{{/if}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
<DiscoveryChain @chain={{chain.Chain}} />

View File

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

View File

@ -1,7 +0,0 @@
{{#if (gt item.Tags.length 0) }}
<TagList @items={{item.Tags}} />
{{else}}
<p>
There are no tags.
</p>
{{/if}}

View File

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

View File

@ -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
(array
(hash id=(slugify 'Service Checks') partial='dc/services/servicechecks')
(hash id=(slugify 'Node Checks') partial='dc/services/nodechecks')
<TabNav @items={{
compact
(array
(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')
)
) key="id" as |panel|
}}
<TabSection @id={{panel.id}} @selected={{eq (if selectedTab selectedTab "") panel.id}} @onchange={{action "change"}}>
{{partial panel.partial}}
</TabSection>
{{/each}}
(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"))
)
}}/>
{{outlet}}
</BlockSlot>
</AppView>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
<div id="routing" class="tab-section">
<div role="tabpanel">
<DiscoveryChain @chain={{chain.Chain}} />
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 |
-------------------------------------------------------
-----------------------------------------------------------------
| Page | Model | Url |
| service | instance | services/service-0-proxy/instances |
-----------------------------------------------------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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