mirror of https://github.com/hashicorp/consul
ui: Redesign Node list page (#8567)
* Create ConsulNodeList component * Implement ConsulNodeList and the new Search/Sort to Node List page * Minor styling fix to align the first icons in composite row * Fix-up and add tests for the redesigned Node List page * Add Leader to composite row for Node List page * Add test for node leaderpull/8586/head
parent
9e1c6727f9
commit
95094b8ddd
|
@ -0,0 +1,41 @@
|
||||||
|
{{#if (gt items.length 0)}}
|
||||||
|
<ListCollection @items={{items}} class="consul-node-list" as |item index|>
|
||||||
|
<BlockSlot @name="header">
|
||||||
|
<dl class={{item.Status}}>
|
||||||
|
<dt>
|
||||||
|
Health
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<Tooltip @position="top-start">
|
||||||
|
{{#if (eq 'critical' item.Status)}}
|
||||||
|
At least one health check on this node is failing.
|
||||||
|
{{else if (eq 'warning' item.Status)}}
|
||||||
|
At least one health check on this node has a warning.
|
||||||
|
{{else if (eq 'passing' item.Status)}}
|
||||||
|
All health checks are passing.
|
||||||
|
{{else}}
|
||||||
|
There are no health checks.
|
||||||
|
{{/if}}
|
||||||
|
</Tooltip>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<a data-test-node href={{href-to "dc.nodes.show" item.Node}}>
|
||||||
|
{{item.Node}}
|
||||||
|
</a>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="details">
|
||||||
|
{{#if (eq item.Address leader.Address)}}
|
||||||
|
<span class="leader" data-test-leader={{leader.Address}}>Leader</span>
|
||||||
|
{{/if}}
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
<CopyButton
|
||||||
|
@value={{item.Address}}
|
||||||
|
@name="Address"
|
||||||
|
/>
|
||||||
|
</dt>
|
||||||
|
<dd>{{item.Address}}</dd>
|
||||||
|
</dl>
|
||||||
|
</BlockSlot>
|
||||||
|
</ListCollection>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
});
|
|
@ -2,26 +2,10 @@ import Controller from '@ember/controller';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
queryParams: {
|
queryParams: {
|
||||||
filterBy: {
|
sortBy: 'sort',
|
||||||
as: 'status',
|
|
||||||
},
|
|
||||||
search: {
|
search: {
|
||||||
as: 'filter',
|
as: 'filter',
|
||||||
replace: true,
|
replace: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
|
||||||
hasStatus: function(status, checks) {
|
|
||||||
if (status === '') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return checks.some(item => item.Status === status);
|
|
||||||
},
|
|
||||||
isHealthy: function(checks) {
|
|
||||||
return !this.actions.isUnhealthy.apply(this, [checks]);
|
|
||||||
},
|
|
||||||
isUnhealthy: function(checks) {
|
|
||||||
return checks.some(item => item.Status === 'critical' || item.Status === 'warning');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,8 +23,7 @@ export function initialize(application) {
|
||||||
policy: policy(filterable),
|
policy: policy(filterable),
|
||||||
role: role(filterable),
|
role: role(filterable),
|
||||||
kv: kv(filterable),
|
kv: kv(filterable),
|
||||||
healthyNode: node(filterable),
|
node: node(filterable),
|
||||||
unhealthyNode: node(filterable),
|
|
||||||
serviceInstance: serviceNode(filterable),
|
serviceInstance: serviceNode(filterable),
|
||||||
nodeservice: nodeService(filterable),
|
nodeservice: nodeService(filterable),
|
||||||
service: service(filterable),
|
service: service(filterable),
|
||||||
|
|
|
@ -6,6 +6,7 @@ import token from 'consul-ui/sort/comparators/token';
|
||||||
import role from 'consul-ui/sort/comparators/role';
|
import role from 'consul-ui/sort/comparators/role';
|
||||||
import policy from 'consul-ui/sort/comparators/policy';
|
import policy from 'consul-ui/sort/comparators/policy';
|
||||||
import nspace from 'consul-ui/sort/comparators/nspace';
|
import nspace from 'consul-ui/sort/comparators/nspace';
|
||||||
|
import node from 'consul-ui/sort/comparators/node';
|
||||||
|
|
||||||
export function initialize(container) {
|
export function initialize(container) {
|
||||||
// Service-less injection using private properties at a per-project level
|
// Service-less injection using private properties at a per-project level
|
||||||
|
@ -19,6 +20,7 @@ export function initialize(container) {
|
||||||
role: role(),
|
role: role(),
|
||||||
policy: policy(),
|
policy: policy(),
|
||||||
nspace: nspace(),
|
nspace: nspace(),
|
||||||
|
node: node(),
|
||||||
};
|
};
|
||||||
Sort.reopen({
|
Sort.reopen({
|
||||||
comparator: function(type) {
|
comparator: function(type) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Model from 'ember-data/model';
|
import Model from 'ember-data/model';
|
||||||
import attr from 'ember-data/attr';
|
import attr from 'ember-data/attr';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
|
||||||
export const PRIMARY_KEY = 'uid';
|
export const PRIMARY_KEY = 'uid';
|
||||||
export const SLUG_KEY = 'ID';
|
export const SLUG_KEY = 'ID';
|
||||||
|
@ -20,4 +21,25 @@ export default Model.extend({
|
||||||
Coord: attr(),
|
Coord: attr(),
|
||||||
SyncTime: attr('number'),
|
SyncTime: attr('number'),
|
||||||
meta: attr(),
|
meta: attr(),
|
||||||
|
Status: computed('Checks.[]', 'ChecksCritical', 'ChecksPassing', 'ChecksWarning', function() {
|
||||||
|
switch (true) {
|
||||||
|
case this.ChecksCritical !== 0:
|
||||||
|
return 'critical';
|
||||||
|
case this.ChecksWarning !== 0:
|
||||||
|
return 'warning';
|
||||||
|
case this.ChecksPassing !== 0:
|
||||||
|
return 'passing';
|
||||||
|
default:
|
||||||
|
return 'empty';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ChecksCritical: computed('Checks.[]', function() {
|
||||||
|
return this.Checks.filter(item => item.Status === 'critical').length;
|
||||||
|
}),
|
||||||
|
ChecksPassing: computed('Checks.[]', function() {
|
||||||
|
return this.Checks.filter(item => item.Status === 'passing').length;
|
||||||
|
}),
|
||||||
|
ChecksWarning: computed('Checks.[]', function() {
|
||||||
|
return this.Checks.filter(item => item.Status === 'warning').length;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ export default Route.extend({
|
||||||
repo: service('repository/node'),
|
repo: service('repository/node'),
|
||||||
data: service('data-source/service'),
|
data: service('data-source/service'),
|
||||||
queryParams: {
|
queryParams: {
|
||||||
|
sortBy: 'sort',
|
||||||
search: {
|
search: {
|
||||||
as: 'filter',
|
as: 'filter',
|
||||||
replace: true,
|
replace: true,
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
export default () => key => {
|
||||||
|
if (key.startsWith('Status:')) {
|
||||||
|
return function(serviceA, serviceB) {
|
||||||
|
const [, dir] = key.split(':');
|
||||||
|
let a, b;
|
||||||
|
if (dir === 'asc') {
|
||||||
|
b = serviceA;
|
||||||
|
a = serviceB;
|
||||||
|
} else {
|
||||||
|
a = serviceA;
|
||||||
|
b = serviceB;
|
||||||
|
}
|
||||||
|
switch (true) {
|
||||||
|
case a.ChecksCritical > b.ChecksCritical:
|
||||||
|
return 1;
|
||||||
|
case a.ChecksCritical < b.ChecksCritical:
|
||||||
|
return -1;
|
||||||
|
default:
|
||||||
|
switch (true) {
|
||||||
|
case a.ChecksWarning > b.ChecksWarning:
|
||||||
|
return 1;
|
||||||
|
case a.ChecksWarning < b.ChecksWarning:
|
||||||
|
return -1;
|
||||||
|
default:
|
||||||
|
switch (true) {
|
||||||
|
case a.ChecksPassing < b.ChecksPassing:
|
||||||
|
return 1;
|
||||||
|
case a.ChecksPassing > b.ChecksPassing:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
};
|
|
@ -27,5 +27,9 @@
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
|
margin-top: 1px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
%reduced-pill.leader::before {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
|
@ -34,3 +34,7 @@
|
||||||
@extend %with-gateway-mask;
|
@extend %with-gateway-mask;
|
||||||
background-color: $gray-500;
|
background-color: $gray-500;
|
||||||
}
|
}
|
||||||
|
%reduced-pill.leader::before {
|
||||||
|
@extend %with-star-outline-mask;
|
||||||
|
background-color: $gray-500;
|
||||||
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ $settings-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fi
|
||||||
$source-file-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.714 9.143l3.572 3.571-3.572 3.572-.714-1.4 2.143-2.172L13 10.571l.714-1.428zm-3.571 1.4L8 12.714l2.143 2.143-.714 1.429-3.572-3.572L9.43 9.143l.714 1.4zm8.571 10.028H4.43V3.43h10l4.285 4.285v12.857zM15.143 2H4.429C3.643 2 3 2.643 3 3.429V20.57C3 21.357 3.643 22 4.429 22h14.285c.786 0 1.429-.643 1.429-1.429V7l-5-5z" fill="%23000"/></svg>');
|
$source-file-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.714 9.143l3.572 3.571-3.572 3.572-.714-1.4 2.143-2.172L13 10.571l.714-1.428zm-3.571 1.4L8 12.714l2.143 2.143-.714 1.429-3.572-3.572L9.43 9.143l.714 1.4zm8.571 10.028H4.43V3.43h10l4.285 4.285v12.857zM15.143 2H4.429C3.643 2 3 2.643 3 3.429V20.57C3 21.357 3.643 22 4.429 22h14.285c.786 0 1.429-.643 1.429-1.429V7l-5-5z" fill="%23000"/></svg>');
|
||||||
$sort-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0,10.0867585 L6,10.0867585 L6,8.40563206 L0,8.40563206 L0,10.0867585 L0,10.0867585 Z M3,12.4056321 L3,14.0867585 L0,14.0867585 L0,12.4056321 L3,12.4056321 Z M15.1301377,0 L15.1301377,1.68112641 L0,1.68112641 L0,0 L15.1301377,0 Z M13.8692929,4.62309763 L13.8692929,11.8384922 L16.8112641,8.89802258 L18,10.0867585 L13.0287297,15.0580288 L8.05745938,10.0867585 L9.24619526,8.89802258 L12.1881665,11.8393328 L12.1881665,4.62309763 L13.8692929,4.62309763 Z M10.0867585,4.20281603 L10.0867585,5.88394244 L0,5.88394244 L0,4.20281603 L10.0867585,4.20281603 Z" fill="%23000"/></svg>');
|
$sort-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0,10.0867585 L6,10.0867585 L6,8.40563206 L0,8.40563206 L0,10.0867585 L0,10.0867585 Z M3,12.4056321 L3,14.0867585 L0,14.0867585 L0,12.4056321 L3,12.4056321 Z M15.1301377,0 L15.1301377,1.68112641 L0,1.68112641 L0,0 L15.1301377,0 Z M13.8692929,4.62309763 L13.8692929,11.8384922 L16.8112641,8.89802258 L18,10.0867585 L13.0287297,15.0580288 L8.05745938,10.0867585 L9.24619526,8.89802258 L12.1881665,11.8393328 L12.1881665,4.62309763 L13.8692929,4.62309763 Z M10.0867585,4.20281603 L10.0867585,5.88394244 L0,5.88394244 L0,4.20281603 L10.0867585,4.20281603 Z" fill="%23000"/></svg>');
|
||||||
$star-fill-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27z" fill="%23000"/></svg>');
|
$star-fill-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27z" fill="%23000"/></svg>');
|
||||||
$star-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z" fill="%23000"/></svg>');
|
$star-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 26.5 26" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 13C1 6.37258 6.37258 1 13 1V1C19.6274 1 25 6.37258 25 13V13C25 19.6274 19.6274 25 13 25V25C6.37258 25 1 19.6274 1 13V13Z" fill="none"/><path fill-rule="evenodd" clip-rule="evenodd" d="M13 16.0742L16.605 18.25L15.6483 14.1492L18.8333 11.39L14.6392 11.0342L13 7.16667L11.3608 11.0342L7.16667 11.39L10.3517 14.1492L9.39501 18.25L13 16.0742Z" fill="%23C62A71"/><path d="M13 24C6.92487 24 2 19.0751 2 13H0C0 20.1797 5.8203 26 13 26V24ZM24 13C24 19.0751 19.0751 24 13 24V26C20.1797 26 26 20.1797 26 13H24ZM13 2C19.0751 2 24 6.92487 24 13H26C26 5.8203 20.1797 0 13 0V2ZM13 0C5.8203 0 0 5.8203 0 13H2C2 6.92487 6.92487 2 13 2V0Z" fill="%23E1E4E7"/></svg>');
|
||||||
$star-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="10" height="9" viewBox="0 0 10 9" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M5 7.196L7.575 8.75l-.683-2.93 2.275-1.97-2.996-.254L5 .833 3.83 3.596.832 3.85l2.275 1.97-.683 2.93z"/></defs><use fill="%239E2159" xlink:href="%23a" fill-rule="evenodd"/></svg>');
|
$star-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="10" height="9" viewBox="0 0 10 9" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M5 7.196L7.575 8.75l-.683-2.93 2.275-1.97-2.996-.254L5 .833 3.83 3.596.832 3.85l2.275 1.97-.683 2.93z"/></defs><use fill="%239E2159" xlink:href="%23a" fill-rule="evenodd"/></svg>');
|
||||||
$sub-left-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.42 9.42L7.83 13H18V4h2v11H7.83l3.59 3.58L10 20l-6-6 6-6 1.42 1.42z" fill="%23000"/></svg>');
|
$sub-left-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.42 9.42L7.83 13H18V4h2v11H7.83l3.59 3.58L10 20l-6-6 6-6 1.42 1.42z" fill="%23000"/></svg>');
|
||||||
$sub-right-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 20l-1.42-1.42L16.17 15H4V4h2v9h10.17l-3.59-3.58L14 8l6 6-6 6z" fill="%23000"/></svg>');
|
$sub-right-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 20l-1.42-1.42L16.17 15H4V4h2v9h10.17l-3.59-3.58L14 8l6 6-6 6z" fill="%23000"/></svg>');
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
}
|
}
|
||||||
%composite-row-icon {
|
%composite-row-icon {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
margin-left: -2px;
|
||||||
}
|
}
|
||||||
%composite-row-icon dt {
|
%composite-row-icon dt {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -6,7 +6,8 @@ td strong {
|
||||||
span.policy-service-identity,
|
span.policy-service-identity,
|
||||||
span.policy-node-identity,
|
span.policy-node-identity,
|
||||||
.consul-external-source,
|
.consul-external-source,
|
||||||
.consul-kind {
|
.consul-kind,
|
||||||
|
.leader {
|
||||||
@extend %reduced-pill;
|
@extend %reduced-pill;
|
||||||
}
|
}
|
||||||
span.policy-service-identity::before,
|
span.policy-service-identity::before,
|
||||||
|
|
|
@ -1,137 +1,74 @@
|
||||||
{{title 'Nodes'}}
|
{{title 'Nodes'}}
|
||||||
<EventSource @src={{items}} />
|
<EventSource @src={{items}} />
|
||||||
{{#let (selectable-key-values
|
{{#let (or sortBy "Node:asc") as |sort|}}
|
||||||
(array "" "All (Any Status)")
|
<AppView @class="node list">
|
||||||
(array "critical" "Critical Checks")
|
<BlockSlot @name="header">
|
||||||
(array "warning" "Warning Checks")
|
<h1>
|
||||||
(array "passing" "Passing Checks")
|
Nodes <em>{{format-number items.length}} total</em>
|
||||||
selected=filterBy
|
</h1>
|
||||||
)
|
<label for="toolbar-toggle"></label>
|
||||||
as |filter|
|
</BlockSlot>
|
||||||
}}
|
<BlockSlot @name="toolbar">
|
||||||
<AppView @class="node list">
|
{{#if (gt items.length 0) }}
|
||||||
<BlockSlot @name="header">
|
|
||||||
<h1>
|
|
||||||
Nodes <em>{{format-number items.length}} total</em>
|
|
||||||
</h1>
|
|
||||||
<label for="toolbar-toggle"></label>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="toolbar">
|
|
||||||
{{#if (gt items.length 0) }}
|
|
||||||
<SearchBar
|
<SearchBar
|
||||||
data-test-catalog-filter
|
|
||||||
@value={{search}}
|
@value={{search}}
|
||||||
@onsearch={{action (mut search) value="target.value"}}
|
@onsearch={{action (mut search) value="target.value"}}
|
||||||
@selected={{filter.selected}}
|
class="with-sort"
|
||||||
@options={{filter.items}}
|
>
|
||||||
@onchange={{action (mut filterBy) value='target.value'}}
|
<BlockSlot @name="secondary">
|
||||||
/>
|
<PopoverSelect
|
||||||
{{/if}}
|
@position="right"
|
||||||
</BlockSlot>
|
@onchange={{action (mut sortBy) value='target.selected'}}
|
||||||
<BlockSlot @name="content">
|
@multiple={{false}}
|
||||||
{{#let (filter-by "Checks" (action "isUnhealthy") items) as |unhealthy|}}
|
as |components|>
|
||||||
{{#if (gt unhealthy.length 0) }}
|
<BlockSlot @name="selected">
|
||||||
<div class="unhealthy">
|
<span>
|
||||||
<h2>Unhealthy Nodes</h2>
|
{{#let (from-entries (array
|
||||||
<div>
|
(array "Node:asc" "A to Z")
|
||||||
{{! think about 2 differing views here }}
|
(array "Node:desc" "Z to A")
|
||||||
<ul>
|
(array "Status:asc" "Unhealthy to Healthy")
|
||||||
<ChangeableSet
|
(array "Status:desc" "Healthy to Unhealthy")
|
||||||
@dispatcher={{
|
))
|
||||||
searchable
|
as |selectable|
|
||||||
'unhealthyNode'
|
}}
|
||||||
(if (eq filter.selected.key "")
|
{{get selectable sort}}
|
||||||
unhealthy
|
{{/let}}
|
||||||
(filter-by "Checks" (action "hasStatus" filter.selected.key) unhealthy filter.selected.key)
|
</span>
|
||||||
)
|
</BlockSlot>
|
||||||
}}
|
<BlockSlot @name="options">
|
||||||
@terms={{search}}
|
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||||
>
|
<Optgroup @label="Name">
|
||||||
<BlockSlot @name="set" as |unhealthy|>
|
<Option @value="Node:asc" @selected={{eq "Node:asc" sort}}>A to Z</Option>
|
||||||
{{#each unhealthy as |item|}}
|
<Option @value="Node:desc" @selected={{eq "Node:desc" sort}}>Z to A</Option>
|
||||||
<HealthcheckedResource @tagName="li" @data-test-node={{item.Node}} @href={{href-to "dc.nodes.show" item.Node}} @name={{item.Node}} @address={{item.Address}} @checks={{item.Checks}}>
|
</Optgroup>
|
||||||
<BlockSlot @name="icon">
|
<Optgroup @label="Health Status">
|
||||||
{{#if (eq item.Address leader.Address)}}
|
<Option @value="Status:asc" @selected={{eq "Status:asc" sort}}>Unhealthy to Healthy</Option>
|
||||||
<span data-test-leader={{leader.Address}} data-tooltip="Leader">Leader</span>
|
<Option @value="Status:desc" @selected={{eq "Status:desc" sort}}>Healthy to Unhealthy</Option>
|
||||||
{{/if}}
|
</Optgroup>
|
||||||
</BlockSlot>
|
{{/let}}
|
||||||
</HealthcheckedResource>
|
</BlockSlot>
|
||||||
{{/each}}
|
</PopoverSelect>
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="empty">
|
|
||||||
<li class="empty">
|
|
||||||
<EmptyState>
|
|
||||||
<BlockSlot @name="header">
|
|
||||||
<h2>No nodes found</h2>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="body">
|
|
||||||
<p>
|
|
||||||
There don't seem to be any nodes matching that search.
|
|
||||||
</p>
|
|
||||||
</BlockSlot>
|
|
||||||
</EmptyState>
|
|
||||||
</li>
|
|
||||||
</BlockSlot>
|
|
||||||
</ChangeableSet>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/let}}
|
|
||||||
{{#let (filter-by "Checks" (action "isHealthy") items) as |healthy|}}
|
|
||||||
{{#if (gt healthy.length 0) }}
|
|
||||||
<div class="healthy">
|
|
||||||
<h2>Healthy Nodes</h2>
|
|
||||||
<ChangeableSet
|
|
||||||
@dispatcher={{
|
|
||||||
searchable
|
|
||||||
'healthyNode'
|
|
||||||
(if (eq filter.selected.key "")
|
|
||||||
healthy
|
|
||||||
(filter-by "Checks" (action "hasStatus" filter.selected.key) healthy filter.selected.key)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
@terms={{search}}
|
|
||||||
>
|
|
||||||
<BlockSlot @name="set" as |healthy|>
|
|
||||||
<GridCollection @cellHeight={{92}} @items={{healthy}} as |item index|>
|
|
||||||
<HealthcheckedResource @data-test-node={{item.Node}} @href={{href-to "dc.nodes.show" item.Node}} @name={{item.Node}} @address={{item.Address}} @checks={{item.Checks}}>
|
|
||||||
<BlockSlot @name="icon">
|
|
||||||
{{#if (eq item.Address leader.Address)}}
|
|
||||||
<span data-test-leader={{leader.Address}} data-tooltip="Leader">Leader</span>
|
|
||||||
{{/if}}
|
|
||||||
</BlockSlot>
|
|
||||||
</HealthcheckedResource>
|
|
||||||
</GridCollection>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="empty">
|
|
||||||
<EmptyState>
|
|
||||||
<BlockSlot @name="header">
|
|
||||||
<h2>No nodes found</h2>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="body">
|
|
||||||
<p>
|
|
||||||
There don't seem to be any nodes matching that search.
|
|
||||||
</p>
|
|
||||||
</BlockSlot>
|
|
||||||
</EmptyState>
|
|
||||||
</BlockSlot>
|
|
||||||
</ChangeableSet>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/let}}
|
|
||||||
{{#if (eq items.length 0) }}
|
|
||||||
<EmptyState @allowLogin={{true}}>
|
|
||||||
<BlockSlot @name="header">
|
|
||||||
<h2>Welcome to Nodes</h2>
|
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
<BlockSlot @name="body">
|
</SearchBar>
|
||||||
<p>
|
{{/if}}
|
||||||
There don't seem to be any nodes, or you may not have access to view nodes yet.
|
</BlockSlot>
|
||||||
</p>
|
<BlockSlot @name="content">
|
||||||
|
{{#let (sort-by (comparator 'node' sort) items) as |sorted|}}
|
||||||
|
<ChangeableSet @dispatcher={{searchable 'node' sorted}} @terms={{search}}>
|
||||||
|
<BlockSlot @name="set" as |filtered|>
|
||||||
|
<ConsulNodeList @items={{filtered}} @leader={{leader}} />
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</EmptyState>
|
<BlockSlot @name="empty">
|
||||||
{{/if}}
|
<EmptyState>
|
||||||
</BlockSlot>
|
<BlockSlot @name="body">
|
||||||
</AppView>
|
<p>
|
||||||
|
There don't seem to be any registered nodes, or you may not have access to view nodes yet.
|
||||||
|
</p>
|
||||||
|
</BlockSlot>
|
||||||
|
</EmptyState>
|
||||||
|
</BlockSlot>
|
||||||
|
</ChangeableSet>
|
||||||
|
{{/let}}
|
||||||
|
</BlockSlot>
|
||||||
|
</AppView>
|
||||||
{{/let}}
|
{{/let}}
|
|
@ -3,68 +3,6 @@
|
||||||
# to use the name filter UI also, then they can stay together
|
# to use the name filter UI also, then they can stay together
|
||||||
@setupApplicationTest
|
@setupApplicationTest
|
||||||
Feature: components / catalog-filter
|
Feature: components / catalog-filter
|
||||||
Scenario: Filtering [Model]
|
|
||||||
Given 1 datacenter model with the value "dc-1"
|
|
||||||
And 4 service models from yaml
|
|
||||||
---
|
|
||||||
- ChecksPassing: 1
|
|
||||||
ChecksWarning: 0
|
|
||||||
ChecksCritical: 0
|
|
||||||
- ChecksPassing: 0
|
|
||||||
ChecksWarning: 1
|
|
||||||
ChecksCritical: 0
|
|
||||||
- ChecksPassing: 0
|
|
||||||
ChecksWarning: 0
|
|
||||||
ChecksCritical: 1
|
|
||||||
- ChecksPassing: 1
|
|
||||||
ChecksWarning: 0
|
|
||||||
ChecksCritical: 0
|
|
||||||
---
|
|
||||||
And 4 node models from yaml
|
|
||||||
---
|
|
||||||
- Checks:
|
|
||||||
- Status: passing
|
|
||||||
- Checks:
|
|
||||||
- Status: warning
|
|
||||||
- Checks:
|
|
||||||
- Status: critical
|
|
||||||
- Checks:
|
|
||||||
- Status: passing
|
|
||||||
---
|
|
||||||
When I visit the [Page] page for yaml
|
|
||||||
---
|
|
||||||
dc: dc-1
|
|
||||||
---
|
|
||||||
Then the url should be [Url]
|
|
||||||
|
|
||||||
Then I see 4 [Model] models
|
|
||||||
And I see allIsSelected on the filter
|
|
||||||
|
|
||||||
When I click passing on the filter
|
|
||||||
And I see passingIsSelected on the filter
|
|
||||||
And I see 2 [Model] models
|
|
||||||
|
|
||||||
When I click warning on the filter
|
|
||||||
And I see warningIsSelected on the filter
|
|
||||||
And I see 1 [Model] model
|
|
||||||
|
|
||||||
When I click critical on the filter
|
|
||||||
And I see criticalIsSelected on the filter
|
|
||||||
And I see 1 [Model] model
|
|
||||||
|
|
||||||
When I click all on the filter
|
|
||||||
And I see allIsSelected on the filter
|
|
||||||
Then I fill in with yaml
|
|
||||||
---
|
|
||||||
s: [Model]-0
|
|
||||||
---
|
|
||||||
And I see 1 [Model] model with the name "[Model]-0"
|
|
||||||
|
|
||||||
Where:
|
|
||||||
-------------------------------------------------
|
|
||||||
| Model | Page | Url |
|
|
||||||
| node | nodes | /dc-1/nodes |
|
|
||||||
-------------------------------------------------
|
|
||||||
Scenario: Filtering [Model] in [Page]
|
Scenario: Filtering [Model] in [Page]
|
||||||
Given 1 datacenter model with the value "dc1"
|
Given 1 datacenter model with the value "dc1"
|
||||||
And 1 node model from yaml
|
And 1 node model from yaml
|
||||||
|
|
|
@ -20,14 +20,11 @@ Feature: dc / nodes / empty-ids: Hedge for if nodes come in over the API with no
|
||||||
dc: dc-1
|
dc: dc-1
|
||||||
---
|
---
|
||||||
Then the url should be /dc-1/nodes
|
Then the url should be /dc-1/nodes
|
||||||
Then I see name on the nodes like yaml
|
Then I see name on the nodes vertically like yaml
|
||||||
---
|
---
|
||||||
- name-1
|
- name-1
|
||||||
- name-2
|
- name-2
|
||||||
- name-3
|
- name-3
|
||||||
- name-4
|
- name-4
|
||||||
- name-5
|
- name-5
|
||||||
|
---
|
||||||
@ignore
|
|
||||||
Scenario: Visually comparing
|
|
||||||
Then the ".unhealthy" element should look like the "/node_modules/@hashicorp/consul-testing-extras/fixtures/dc/nodes/empty-ids.png" image
|
|
|
@ -16,7 +16,7 @@ Feature: dc / nodes / index
|
||||||
Then the url should be /dc-1/nodes
|
Then the url should be /dc-1/nodes
|
||||||
And the title should be "Nodes - Consul"
|
And the title should be "Nodes - Consul"
|
||||||
Then I see 3 node models
|
Then I see 3 node models
|
||||||
Scenario: Seeing the leader in unhealthy listing
|
Scenario: Seeing the leader in node listing
|
||||||
Given 3 node models from yaml
|
Given 3 node models from yaml
|
||||||
---
|
---
|
||||||
- Address: 211.245.86.75
|
- Address: 211.245.86.75
|
||||||
|
@ -32,24 +32,7 @@ Feature: dc / nodes / index
|
||||||
---
|
---
|
||||||
Then the url should be /dc-1/nodes
|
Then the url should be /dc-1/nodes
|
||||||
Then I see 3 node models
|
Then I see 3 node models
|
||||||
And I see leader on the unHealthyNodes
|
And I see leader on the nodes.0
|
||||||
Scenario: Seeing the leader in healthy listing
|
|
||||||
Given 3 node models from yaml
|
|
||||||
---
|
|
||||||
- Address: 211.245.86.75
|
|
||||||
Checks:
|
|
||||||
- Status: passing
|
|
||||||
Name: Passing check
|
|
||||||
- Address: 10.0.0.1
|
|
||||||
- Address: 10.0.0.3
|
|
||||||
---
|
|
||||||
When I visit the nodes page for yaml
|
|
||||||
---
|
|
||||||
dc: dc-1
|
|
||||||
---
|
|
||||||
Then the url should be /dc-1/nodes
|
|
||||||
Then I see 3 node models
|
|
||||||
And I see leader on the healthyNodes
|
|
||||||
Scenario: Searching the nodes with name and IP address
|
Scenario: Searching the nodes with name and IP address
|
||||||
Given 3 node models from yaml
|
Given 3 node models from yaml
|
||||||
---
|
---
|
||||||
|
@ -76,4 +59,4 @@ Feature: dc / nodes / index
|
||||||
s: 10.0.0.1
|
s: 10.0.0.1
|
||||||
---
|
---
|
||||||
And I see 1 node model
|
And I see 1 node model
|
||||||
And I see 1 node model with the name "node-02"
|
And I see 1 node model with the name "node-02"
|
|
@ -0,0 +1,16 @@
|
||||||
|
@setupApplicationTest
|
||||||
|
Feature: dc / nodes / navigation
|
||||||
|
Scenario: Clicking a node in the listing and back again
|
||||||
|
Given 1 datacenter model with the value "dc-1"
|
||||||
|
And 3 node models
|
||||||
|
When I visit the nodes page for yaml
|
||||||
|
---
|
||||||
|
dc: dc-1
|
||||||
|
---
|
||||||
|
Then the url should be /dc-1/nodes
|
||||||
|
And the title should be "Nodes - Consul"
|
||||||
|
Then I see 3 node models
|
||||||
|
When I click node on the nodes
|
||||||
|
And I click "[data-test-back]"
|
||||||
|
Then the url should be /dc-1/nodes
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
@setupApplicationTest
|
||||||
|
Feature: dc / nodes / sorting
|
||||||
|
Scenario:
|
||||||
|
Given 1 datacenter model with the value "dc-1"
|
||||||
|
And 6 node models from yaml
|
||||||
|
---
|
||||||
|
- Node: Node-A
|
||||||
|
Checks:
|
||||||
|
- Status: critical
|
||||||
|
- Node: Node-B
|
||||||
|
Checks:
|
||||||
|
- Status: passing
|
||||||
|
- Node: Node-C
|
||||||
|
Checks:
|
||||||
|
- Status: warning
|
||||||
|
- Node: Node-D
|
||||||
|
Checks:
|
||||||
|
- Status: critical
|
||||||
|
- Node: Node-E
|
||||||
|
Checks:
|
||||||
|
- Status: critical
|
||||||
|
- Node: Node-F
|
||||||
|
Checks:
|
||||||
|
- Status: warning
|
||||||
|
---
|
||||||
|
When I visit the nodes page for yaml
|
||||||
|
---
|
||||||
|
dc: dc-1
|
||||||
|
---
|
||||||
|
When I click selected on the sort
|
||||||
|
When I click options.3.button on the sort
|
||||||
|
Then I see name on the nodes vertically like yaml
|
||||||
|
---
|
||||||
|
- Node-B
|
||||||
|
- Node-C
|
||||||
|
- Node-F
|
||||||
|
- Node-A
|
||||||
|
- Node-D
|
||||||
|
- Node-E
|
||||||
|
---
|
||||||
|
When I click selected on the sort
|
||||||
|
When I click options.2.button on the sort
|
||||||
|
Then I see name on the nodes vertically like yaml
|
||||||
|
---
|
||||||
|
- Node-A
|
||||||
|
- Node-D
|
||||||
|
- Node-E
|
||||||
|
- Node-C
|
||||||
|
- Node-F
|
||||||
|
- Node-B
|
||||||
|
---
|
||||||
|
When I click selected on the sort
|
||||||
|
When I click options.0.button on the sort
|
||||||
|
Then I see name on the nodes vertically like yaml
|
||||||
|
---
|
||||||
|
- Node-A
|
||||||
|
- Node-B
|
||||||
|
- Node-C
|
||||||
|
- Node-D
|
||||||
|
- Node-E
|
||||||
|
- Node-F
|
||||||
|
---
|
||||||
|
When I click selected on the sort
|
||||||
|
When I click options.1.button on the sort
|
||||||
|
Then I see name on the nodes vertically like yaml
|
||||||
|
---
|
||||||
|
- Node-F
|
||||||
|
- Node-E
|
||||||
|
- Node-D
|
||||||
|
- Node-C
|
||||||
|
- Node-B
|
||||||
|
- Node-A
|
||||||
|
---
|
|
@ -43,7 +43,6 @@ Feature: page-navigation
|
||||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
| Item | Model | URL | Endpoint | Back |
|
| Item | Model | URL | Endpoint | Back |
|
||||||
| service | services | /dc-1/services/service-0/instances | /v1/discovery-chain/service-0?dc=dc-1&ns=@namespace | /dc-1/services |
|
| 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 |
|
| 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 |
|
# | acl | acls | /dc-1/acls/anonymous | /v1/acl/info/anonymous?dc=dc-1 | /dc-1/acls |
|
||||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import steps from '../../steps';
|
||||||
|
|
||||||
|
// step definitions that are shared between features should be moved to the
|
||||||
|
// tests/acceptance/steps/steps.js file
|
||||||
|
|
||||||
|
export default function(assert) {
|
||||||
|
return steps(assert).then('I should find a file', function() {
|
||||||
|
assert.ok(true, this.step);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import steps from '../../steps';
|
||||||
|
|
||||||
|
// step definitions that are shared between features should be moved to the
|
||||||
|
// tests/acceptance/steps/steps.js file
|
||||||
|
|
||||||
|
export default function(assert) {
|
||||||
|
return steps(assert).then('I should find a file', function() {
|
||||||
|
assert.ok(true, this.step);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import steps from '../steps';
|
||||||
|
|
||||||
|
// step definitions that are shared between features should be moved to the
|
||||||
|
// tests/acceptance/steps/steps.js file
|
||||||
|
|
||||||
|
export default function(assert) {
|
||||||
|
return steps(assert).then('I should find a file', function() {
|
||||||
|
assert.ok(true, this.step);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { module, skip } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
import { render } from '@ember/test-helpers';
|
||||||
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
|
||||||
|
module('Integration | Component | consul-node-list', function(hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
skip('it renders', async function(assert) {
|
||||||
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
|
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||||
|
|
||||||
|
await render(hbs`<ConsulNodeList />`);
|
||||||
|
|
||||||
|
assert.equal(this.element.textContent.trim(), '');
|
||||||
|
|
||||||
|
// Template block usage:
|
||||||
|
await render(hbs`
|
||||||
|
<ConsulNodeList>
|
||||||
|
template block text
|
||||||
|
</ConsulNodeList>
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.equal(this.element.textContent.trim(), 'template block text');
|
||||||
|
});
|
||||||
|
});
|
|
@ -79,9 +79,6 @@ const tokenList = tokenListFactory(clickable, attribute, collection, deletable);
|
||||||
const authForm = authFormFactory(submitable, clickable, attribute);
|
const authForm = authFormFactory(submitable, clickable, attribute);
|
||||||
const freetextFilter = freetextFilterFactory(triggerable);
|
const freetextFilter = freetextFilterFactory(triggerable);
|
||||||
const catalogToolbar = searchBarFactory(freetextFilter);
|
const catalogToolbar = searchBarFactory(freetextFilter);
|
||||||
const catalogFilter = searchBarFactory(freetextFilter, () =>
|
|
||||||
radiogroup('status', ['', 'passing', 'warning', 'critical'])
|
|
||||||
);
|
|
||||||
const aclFilter = searchBarFactory(freetextFilter, () =>
|
const aclFilter = searchBarFactory(freetextFilter, () =>
|
||||||
radiogroup('type', ['', 'management', 'client'])
|
radiogroup('type', ['', 'management', 'client'])
|
||||||
);
|
);
|
||||||
|
@ -153,7 +150,7 @@ export default {
|
||||||
service(visitable, attribute, collection, text, consulIntentionList, catalogToolbar, tabgroup)
|
service(visitable, attribute, collection, text, consulIntentionList, catalogToolbar, tabgroup)
|
||||||
),
|
),
|
||||||
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
|
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
|
||||||
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
|
nodes: create(nodes(visitable, text, clickable, attribute, collection, popoverSelect)),
|
||||||
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup, text)),
|
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup, text)),
|
||||||
kvs: create(kvs(visitable, creatable, consulKvList)),
|
kvs: create(kvs(visitable, creatable, consulKvList)),
|
||||||
kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)),
|
kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)),
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
export default function(visitable, clickable, attribute, collection, filter) {
|
export default function(visitable, text, clickable, attribute, collection, popoverSelect) {
|
||||||
const node = {
|
const node = {
|
||||||
name: attribute('data-test-node'),
|
name: text('[data-test-node]'),
|
||||||
leader: attribute('data-test-leader', '[data-test-leader]'),
|
leader: attribute('data-test-leader', '[data-test-leader]'),
|
||||||
node: clickable('header a'),
|
node: clickable('a'),
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
visit: visitable('/:dc/nodes'),
|
visit: visitable('/:dc/nodes'),
|
||||||
nodes: collection('[data-test-node]', node),
|
nodes: collection('.consul-node-list [data-test-list-row]', node),
|
||||||
healthyNodes: collection('.healthy [data-test-node]', node),
|
home: clickable('[data-test-home]'),
|
||||||
unHealthyNodes: collection('.unhealthy [data-test-node]', node),
|
sort: popoverSelect(),
|
||||||
filter: filter('[data-test-catalog-filter]'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue