ui: Leader icon for node listing view (#6265)
- yarn upgrade consul-api-double which includes `status/leader` - add all the ember-data things required to call a new endpoint - Pass the new leader variable through to the template - use the new leader variable in the template to set a leader - add acceptance testing to verify leaders are highlighted - Change testing navigation/api requests to status/leader (on the node listing page, status/leader is now the last get request to be called). - Template whitespace commit (less indenting) - adds a test to to assert no errors happen with an unelected leader
This commit is contained in:
parent
7db707c8b3
commit
5cf063fc8a
|
@ -20,6 +20,38 @@ export default Adapter.extend({
|
|||
}
|
||||
return this.appendURL('internal/ui/node', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
urlForRequest: function({ type, snapshot, requestType }) {
|
||||
switch (requestType) {
|
||||
case 'queryLeader':
|
||||
return this.urlForQueryLeader(snapshot, type.modelName);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
urlForQueryLeader: function(query, modelName) {
|
||||
// https://www.consul.io/api/status.html#get-raft-leader
|
||||
return this.appendURL('status/leader', [], this.cleanQuery(query));
|
||||
},
|
||||
isQueryLeader: function(url, method) {
|
||||
return url.pathname === this.parseURL(this.urlForQueryLeader({})).pathname;
|
||||
},
|
||||
queryLeader: function(store, modelClass, id, snapshot) {
|
||||
const params = {
|
||||
store: store,
|
||||
type: modelClass,
|
||||
id: id,
|
||||
snapshot: snapshot,
|
||||
requestType: 'queryLeader',
|
||||
};
|
||||
// _requestFor is private... but these methods aren't, until they disappear..
|
||||
const request = {
|
||||
method: this.methodForRequest(params),
|
||||
url: this.urlForRequest(params),
|
||||
headers: this.headersForRequest(params),
|
||||
data: this.dataForRequest(params),
|
||||
};
|
||||
// TODO: private..
|
||||
return this._makeRequest(request);
|
||||
},
|
||||
handleBatchResponse: function(url, response, primary, slug) {
|
||||
const dc = url.searchParams.get(API_DATACENTER_KEY) || '';
|
||||
return response.map((item, i, arr) => {
|
||||
|
@ -41,7 +73,21 @@ export default Adapter.extend({
|
|||
const method = requestData.method;
|
||||
if (status === HTTP_OK) {
|
||||
const url = this.parseURL(requestData.url);
|
||||
let temp, port, address;
|
||||
switch (true) {
|
||||
case this.isQueryLeader(url, method):
|
||||
// This response is just an ip:port like `"10.0.0.1:8000"`
|
||||
// split it and make it look like a `C`onsul.`R`esponse
|
||||
// popping off the end for ports should cover us for IPv6 addresses
|
||||
// as we should always get a `address:port` or `[a:dd:re:ss]:port` combo
|
||||
temp = response.split(':');
|
||||
port = temp.pop();
|
||||
address = temp.join(':');
|
||||
response = {
|
||||
Address: address,
|
||||
Port: port,
|
||||
};
|
||||
break;
|
||||
case this.isQueryRecord(url, method):
|
||||
response = this.handleSingleResponse(url, fillSlug(response), PRIMARY_KEY, SLUG_KEY);
|
||||
break;
|
||||
|
|
|
@ -12,8 +12,10 @@ export default Route.extend({
|
|||
},
|
||||
},
|
||||
model: function(params) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
return hash({
|
||||
items: get(this, 'repo').findAllByDatacenter(this.modelFor('dc').dc.Name),
|
||||
items: get(this, 'repo').findAllByDatacenter(dc),
|
||||
leader: get(this, 'repo').findByLeader(dc),
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
const modelName = 'node';
|
||||
export default RepositoryService.extend({
|
||||
coordinates: service('repository/coordinate'),
|
||||
getModelName: function() {
|
||||
return modelName;
|
||||
},
|
||||
findByLeader: function(dc) {
|
||||
const query = {
|
||||
dc: dc,
|
||||
};
|
||||
return get(this, 'store').queryLeader(this.getModelName(), query);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ const createProxy = function(repo, find, settings, cache, serialize = JSON.strin
|
|||
type: 'message',
|
||||
data: result,
|
||||
};
|
||||
const meta = get(event.data || {}, 'meta');
|
||||
const meta = get(event.data || {}, 'meta') || {};
|
||||
if (typeof meta.date !== 'undefined') {
|
||||
// unload anything older than our current sync date/time
|
||||
store.peekAll(repo.getModelName()).forEach(function(item) {
|
||||
|
|
|
@ -23,4 +23,9 @@ export default Store.extend({
|
|||
const adapter = this.adapterFor(modelName);
|
||||
return adapter.self(this, { modelName: modelName }, token);
|
||||
},
|
||||
queryLeader: function(modelName, query) {
|
||||
// TODO: no normalization, type it properly for the moment
|
||||
const adapter = this.adapterFor(modelName);
|
||||
return adapter.queryLeader(this, { modelName: modelName }, null, query);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
.healthchecked-resource > div {
|
||||
@extend %stats-card;
|
||||
}
|
||||
|
||||
%tooltip-below::after {
|
||||
top: calc(100% - 8px);
|
||||
bottom: auto;
|
||||
|
@ -12,6 +11,8 @@
|
|||
%tooltip-below::before {
|
||||
top: calc(100% + 4px);
|
||||
bottom: auto;
|
||||
/*TODO: This should probably go into base*/
|
||||
line-height: 1em;
|
||||
}
|
||||
%tooltip-left::before {
|
||||
right: 0;
|
||||
|
@ -21,8 +22,6 @@
|
|||
}
|
||||
%stats-card-icon {
|
||||
@extend %tooltip-below;
|
||||
/*TODO: This should probably go into base*/
|
||||
line-height: 1em;
|
||||
}
|
||||
%stats-card-icon:first-child::before {
|
||||
right: 0;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{#stats-card}}
|
||||
{{#block-slot 'icon'}}{{#if false}}<span data-tooltip="Leader">Leader</span>{{/if}}{{/block-slot}}
|
||||
{{#block-slot 'icon'}}{{yield}}{{/block-slot}}
|
||||
{{#block-slot 'mini-stat'}}
|
||||
{{#if (eq checks.length 0)}}
|
||||
<span class="zero" data-tooltip="This node has no registered healthchecks">{{checks.length}}</span>
|
||||
|
|
|
@ -1,72 +1,84 @@
|
|||
{{#app-view class="node list"}}
|
||||
{{#block-slot 'header'}}
|
||||
<h1>
|
||||
Nodes <em>{{format-number items.length}} total</em>
|
||||
</h1>
|
||||
<label for="toolbar-toggle"></label>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'toolbar'}}
|
||||
{{#block-slot 'header'}}
|
||||
<h1>
|
||||
Nodes <em>{{format-number items.length}} total</em>
|
||||
</h1>
|
||||
<label for="toolbar-toggle"></label>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'toolbar'}}
|
||||
{{#if (gt items.length 0) }}
|
||||
{{catalog-filter searchable=(array searchableHealthy searchableUnhealthy) search=s status=filters.status onchange=(action 'filter')}}
|
||||
{{catalog-filter searchable=(array searchableHealthy searchableUnhealthy) search=s status=filters.status onchange=(action 'filter')}}
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'content'}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'content'}}
|
||||
{{#if (gt unhealthy.length 0) }}
|
||||
<div class="unhealthy">
|
||||
<h2>Unhealthy Nodes</h2>
|
||||
<div>
|
||||
{{! think about 2 differing views here }}
|
||||
<ul>
|
||||
{{#changeable-set dispatcher=searchableUnhealthy}}
|
||||
{{#block-slot 'set' as |unhealthy|}}
|
||||
{{#each unhealthy as |item|}}
|
||||
{{healthchecked-resource
|
||||
tagName='li'
|
||||
data-test-node=item.Node
|
||||
href=(href-to 'dc.nodes.show' item.Node)
|
||||
name=item.Node
|
||||
address=item.Address
|
||||
checks=item.Checks
|
||||
}}
|
||||
{{/each}}
|
||||
<div class="unhealthy">
|
||||
<h2>Unhealthy Nodes</h2>
|
||||
<div>
|
||||
{{! think about 2 differing views here }}
|
||||
<ul>
|
||||
{{#changeable-set dispatcher=searchableUnhealthy}}
|
||||
{{#block-slot 'set' as |unhealthy|}}
|
||||
{{#each unhealthy as |item|}}
|
||||
{{#healthchecked-resource
|
||||
tagName='li'
|
||||
data-test-node=item.Node
|
||||
href=(href-to 'dc.nodes.show' item.Node)
|
||||
name=item.Node
|
||||
address=item.Address
|
||||
checks=item.Checks
|
||||
}}
|
||||
{{#block-slot 'icon'}}
|
||||
{{#if (eq item.Address leader.Address)}}
|
||||
<span data-test-leader={{leader.Address}} data-tooltip="Leader">Leader</span>
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no unhealthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (gt healthy.length 0) }}
|
||||
<div class="healthy">
|
||||
<h2>Healthy Nodes</h2>
|
||||
{{#changeable-set dispatcher=searchableHealthy}}
|
||||
{{#block-slot 'set' as |healthy|}}
|
||||
{{#list-collection cellHeight=92 items=healthy as |item index|}}
|
||||
{{healthchecked-resource
|
||||
data-test-node=item.Node
|
||||
href=(href-to 'dc.nodes.show' item.Node)
|
||||
name=item.Node
|
||||
address=item.Address
|
||||
checks=item.Checks
|
||||
}}
|
||||
{{/list-collection}}
|
||||
{{/healthchecked-resource}}
|
||||
{{/each}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no healthy nodes for that search.
|
||||
There are no unhealthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (gt healthy.length 0) }}
|
||||
<div class="healthy">
|
||||
<h2>Healthy Nodes</h2>
|
||||
{{#changeable-set dispatcher=searchableHealthy}}
|
||||
{{#block-slot 'set' as |healthy|}}
|
||||
{{#list-collection cellHeight=92 items=healthy as |item index|}}
|
||||
{{#healthchecked-resource
|
||||
data-test-node=item.Node
|
||||
href=(href-to 'dc.nodes.show' item.Node)
|
||||
name=item.Node
|
||||
address=item.Address
|
||||
checks=item.Checks
|
||||
}}
|
||||
{{#block-slot 'icon'}}
|
||||
{{#if (eq item.Address leader.Address)}}
|
||||
<span data-test-leader={{leader.Address}} data-tooltip="Leader">Leader</span>
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
{{/healthchecked-resource}}
|
||||
{{/list-collection}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no healthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (and (eq healthy.length 0) (eq unhealthy.length 0)) }}
|
||||
<p>
|
||||
There are no nodes.
|
||||
</p>
|
||||
<p>
|
||||
There are no nodes.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
{{/block-slot}}
|
||||
{{/app-view}}
|
|
@ -1,11 +1,51 @@
|
|||
@setupApplicationTest
|
||||
Feature: Nodes
|
||||
Scenario:
|
||||
Feature: dc / nodes / index
|
||||
Background:
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 3 node models
|
||||
And the url "/v1/status/leader" responds with from yaml
|
||||
---
|
||||
body: |
|
||||
"211.245.86.75:8500"
|
||||
---
|
||||
Scenario: Viewing nodes in the listing
|
||||
Given 3 node models
|
||||
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
|
||||
Scenario: Seeing the leader in unhealthy listing
|
||||
Given 3 node models from yaml
|
||||
---
|
||||
- Address: 211.245.86.75
|
||||
Checks:
|
||||
- Status: warning
|
||||
Name: Warning 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 unHealthyNodes
|
||||
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
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / nodes / no-leader
|
||||
Scenario: Leader hasn't been elected
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 3 node models
|
||||
And the url "/v1/status/leader" responds with from yaml
|
||||
---
|
||||
body: |
|
||||
""
|
||||
---
|
||||
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 don't see leader on the nodes
|
||||
|
|
@ -23,7 +23,7 @@ Feature: Page Navigation
|
|||
Where:
|
||||
-----------------------------------------------------------------------
|
||||
| Link | URL | Endpoint |
|
||||
| nodes | /dc-1/nodes | /v1/internal/ui/nodes?dc=dc-1 |
|
||||
| nodes | /dc-1/nodes | /v1/status/leader?dc=dc-1 |
|
||||
| kvs | /dc-1/kv | /v1/kv/?keys&dc=dc-1&separator=%2F |
|
||||
| acls | /dc-1/acls/tokens | /v1/acl/tokens?dc=dc-1 |
|
||||
| intentions | /dc-1/intentions | /v1/connect/intentions?dc=dc-1 |
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
export default function(visitable, clickable, attribute, collection, filter) {
|
||||
const node = {
|
||||
name: attribute('data-test-node'),
|
||||
leader: attribute('data-test-leader', '[data-test-leader]'),
|
||||
node: clickable('header a'),
|
||||
};
|
||||
return {
|
||||
visit: visitable('/:dc/nodes'),
|
||||
nodes: collection('[data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
node: clickable('header a'),
|
||||
}),
|
||||
nodes: collection('[data-test-node]', node),
|
||||
healthyNodes: collection('.healthy [data-test-node]', node),
|
||||
unHealthyNodes: collection('.unhealthy [data-test-node]', node),
|
||||
filter: filter,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -880,9 +880,9 @@
|
|||
js-yaml "^3.13.1"
|
||||
|
||||
"@hashicorp/consul-api-double@^2.0.1":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.3.0.tgz#1163f6dacb29d43d8dac4d1473263c257321682a"
|
||||
integrity sha512-wbaOyOoA1X5Ur7Gj4VSZkor1zuJ2+GTbavPJGtpZZXd6CtL3RXC4HaldruBIF79j3lBXVgS/Y9ETMfGLdoAYgA==
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.5.0.tgz#d9540a38ee652d55ed90956850c9e5cbcde89454"
|
||||
integrity sha512-RZcVIPQ4M4TZzFe2mWm7M5w28yOIpVgiYZI5ax+JG0Yr5TVbhJPMxhdb1es73cILuqIi9Fr+73OJ5IAospgPBw==
|
||||
|
||||
"@hashicorp/ember-cli-api-double@^2.0.0":
|
||||
version "2.0.0"
|
||||
|
|
Loading…
Reference in New Issue