From 659ff864e30e5b97b9446448e079a8c7b8c23af1 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 2 Aug 2019 11:10:52 +0000 Subject: [PATCH 1/8] ui: yarn upgrade consul-aoi-double which includes `status/leader` --- ui-v2/package.json | 4 ++-- ui-v2/yarn.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui-v2/package.json b/ui-v2/package.json index eb67171b05d2..beddf8b88aca 100644 --- a/ui-v2/package.json +++ b/ui-v2/package.json @@ -52,6 +52,7 @@ "base64-js": "^1.3.0", "broccoli-asset-rev": "^2.4.5", "chalk": "^2.4.2", + "clipboard": "^2.0.4", "dart-sass": "^1.14.1", "ember-ajax": "^3.0.0", "ember-auto-import": "^1.4.0", @@ -103,8 +104,7 @@ "node-sass": "^4.9.3", "prettier": "^1.10.2", "svgo": "^1.0.5", - "text-encoding": "^0.6.4", - "clipboard": "^2.0.4" + "text-encoding": "^0.6.4" }, "engines": { "node": "^4.5 || 6.* || >= 7.*" diff --git a/ui-v2/yarn.lock b/ui-v2/yarn.lock index 74144f5ee3cf..2dc908027408 100644 --- a/ui-v2/yarn.lock +++ b/ui-v2/yarn.lock @@ -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" From ec751e0d67160904f6f967b3fe82120740948940 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 2 Aug 2019 11:12:42 +0000 Subject: [PATCH 2/8] ui: add all the ember-data things required to call a new endpoint --- ui-v2/app/adapters/node.js | 46 +++++++++++++++++++++++++++ ui-v2/app/services/repository/node.js | 8 +++++ ui-v2/app/services/store.js | 5 +++ 3 files changed, 59 insertions(+) diff --git a/ui-v2/app/adapters/node.js b/ui-v2/app/adapters/node.js index 3b1cace76b5f..7e7ba7514a5c 100644 --- a/ui-v2/app/adapters/node.js +++ b/ui-v2/app/adapters/node.js @@ -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; diff --git a/ui-v2/app/services/repository/node.js b/ui-v2/app/services/repository/node.js index eec4f211d3eb..70ee56e30842 100644 --- a/ui-v2/app/services/repository/node.js +++ b/ui-v2/app/services/repository/node.js @@ -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); + }, }); diff --git a/ui-v2/app/services/store.js b/ui-v2/app/services/store.js index 640e1b7d1376..081508e9a3ed 100644 --- a/ui-v2/app/services/store.js +++ b/ui-v2/app/services/store.js @@ -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); + }, }); From 791e25d2d578943909cf0f076f4f16d5ea3f525f Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 2 Aug 2019 11:13:59 +0000 Subject: [PATCH 3/8] ui: Pass the new leader variable through to the template --- ui-v2/app/routes/dc/nodes/index.js | 4 +++- ui-v2/app/services/repository/type/event-source.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui-v2/app/routes/dc/nodes/index.js b/ui-v2/app/routes/dc/nodes/index.js index 8cbd56ecdc04..2852d3dff642 100644 --- a/ui-v2/app/routes/dc/nodes/index.js +++ b/ui-v2/app/routes/dc/nodes/index.js @@ -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) { diff --git a/ui-v2/app/services/repository/type/event-source.js b/ui-v2/app/services/repository/type/event-source.js index 31c1b41ea9b5..3999abf7a317 100644 --- a/ui-v2/app/services/repository/type/event-source.js +++ b/ui-v2/app/services/repository/type/event-source.js @@ -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) { From d96110406863bed57c525b477689bb4cad6742a4 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 2 Aug 2019 11:14:41 +0000 Subject: [PATCH 4/8] ui: use the new leader variable in the template to set a leader --- .../components/healthchecked-resource.scss | 5 ++--- .../components/healthchecked-resource.hbs | 2 +- ui-v2/app/templates/dc/nodes/index.hbs | 16 ++++++++++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ui-v2/app/styles/components/healthchecked-resource.scss b/ui-v2/app/styles/components/healthchecked-resource.scss index 88937a038606..80fa42d228ea 100644 --- a/ui-v2/app/styles/components/healthchecked-resource.scss +++ b/ui-v2/app/styles/components/healthchecked-resource.scss @@ -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; diff --git a/ui-v2/app/templates/components/healthchecked-resource.hbs b/ui-v2/app/templates/components/healthchecked-resource.hbs index b3fc52b3f2fb..465f26eb0fe5 100644 --- a/ui-v2/app/templates/components/healthchecked-resource.hbs +++ b/ui-v2/app/templates/components/healthchecked-resource.hbs @@ -1,5 +1,5 @@ {{#stats-card}} - {{#block-slot 'icon'}}{{#if false}}Leader{{/if}}{{/block-slot}} + {{#block-slot 'icon'}}{{yield}}{{/block-slot}} {{#block-slot 'mini-stat'}} {{#if (eq checks.length 0)}} {{checks.length}} diff --git a/ui-v2/app/templates/dc/nodes/index.hbs b/ui-v2/app/templates/dc/nodes/index.hbs index 0e46adfccfc8..8ac3b2456e3d 100644 --- a/ui-v2/app/templates/dc/nodes/index.hbs +++ b/ui-v2/app/templates/dc/nodes/index.hbs @@ -20,7 +20,7 @@ {{#changeable-set dispatcher=searchableUnhealthy}} {{#block-slot 'set' as |unhealthy|}} {{#each unhealthy as |item|}} - {{healthchecked-resource + {{#healthchecked-resource tagName='li' data-test-node=item.Node href=(href-to 'dc.nodes.show' item.Node) @@ -28,6 +28,12 @@ address=item.Address checks=item.Checks }} + {{#block-slot 'icon'}} + {{#if (eq item.Address leader.Address)}} + Leader + {{/if}} + {{/block-slot}} + {{/healthchecked-resource}} {{/each}} {{/block-slot}} {{#block-slot 'empty'}} @@ -46,13 +52,19 @@ {{#changeable-set dispatcher=searchableHealthy}} {{#block-slot 'set' as |healthy|}} {{#list-collection cellHeight=92 items=healthy as |item index|}} - {{healthchecked-resource + {{#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)}} + Leader + {{/if}} + {{/block-slot}} + {{/healthchecked-resource}} {{/list-collection}} {{/block-slot}} {{#block-slot 'empty'}} From 2300903e22aaa3a605e136d114fd8491d85b89d2 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 2 Aug 2019 11:15:32 +0000 Subject: [PATCH 5/8] ui: add acceptence testing to verify leaders are highlighted --- ui-v2/tests/acceptance/dc/nodes/index.feature | 46 +++++++++++++++++-- ui-v2/tests/pages/dc/nodes/index.js | 12 +++-- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/ui-v2/tests/acceptance/dc/nodes/index.feature b/ui-v2/tests/acceptance/dc/nodes/index.feature index 4e6c4d063772..9eb963024d71 100644 --- a/ui-v2/tests/acceptance/dc/nodes/index.feature +++ b/ui-v2/tests/acceptance/dc/nodes/index.feature @@ -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 diff --git a/ui-v2/tests/pages/dc/nodes/index.js b/ui-v2/tests/pages/dc/nodes/index.js index 1899e7df5ecd..25045ec38850 100644 --- a/ui-v2/tests/pages/dc/nodes/index.js +++ b/ui-v2/tests/pages/dc/nodes/index.js @@ -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, }; } From a73307acf829e342b520d0ffb6034a0ff1043b89 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 2 Aug 2019 11:21:40 +0000 Subject: [PATCH 6/8] ui: Change testing navigation/api requests to status/leader On the node listing page, status/leader is now the last get request to be called. --- ui-v2/tests/acceptance/page-navigation.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-v2/tests/acceptance/page-navigation.feature b/ui-v2/tests/acceptance/page-navigation.feature index 1a133322235f..1fd782123165 100644 --- a/ui-v2/tests/acceptance/page-navigation.feature +++ b/ui-v2/tests/acceptance/page-navigation.feature @@ -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 | From 555103ce37534cf633901f66b3c40424091ed414 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 2 Aug 2019 13:43:17 +0000 Subject: [PATCH 7/8] ui: Template whitespace commit (less indenting) --- ui-v2/app/templates/dc/nodes/index.hbs | 136 ++++++++++++------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/ui-v2/app/templates/dc/nodes/index.hbs b/ui-v2/app/templates/dc/nodes/index.hbs index 8ac3b2456e3d..761cc4830fd3 100644 --- a/ui-v2/app/templates/dc/nodes/index.hbs +++ b/ui-v2/app/templates/dc/nodes/index.hbs @@ -1,84 +1,84 @@ {{#app-view class="node list"}} - {{#block-slot 'header'}} -

- Nodes {{format-number items.length}} total -

- - {{/block-slot}} - {{#block-slot 'toolbar'}} + {{#block-slot 'header'}} +

+ Nodes {{format-number items.length}} total +

+ + {{/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) }} -
-

Unhealthy Nodes

-
- {{! think about 2 differing views here }} -
    - {{#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)}} - Leader - {{/if}} - {{/block-slot}} - {{/healthchecked-resource}} - {{/each}} +
    +

    Unhealthy Nodes

    +
    + {{! think about 2 differing views here }} +
      + {{#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)}} + Leader + {{/if}} {{/block-slot}} - {{#block-slot 'empty'}} -

      - There are no unhealthy nodes for that search. -

      - {{/block-slot}} - {{/changeable-set}} -
    -
    -
    -{{/if}} -{{#if (gt healthy.length 0) }} -
    -

    Healthy Nodes

    - {{#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)}} - Leader - {{/if}} - {{/block-slot}} - {{/healthchecked-resource}} - {{/list-collection}} + {{/healthchecked-resource}} + {{/each}} {{/block-slot}} {{#block-slot 'empty'}}

    - There are no healthy nodes for that search. + There are no unhealthy nodes for that search.

    {{/block-slot}} {{/changeable-set}} +
+
+{{/if}} +{{#if (gt healthy.length 0) }} +
+

Healthy Nodes

+ {{#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)}} + Leader + {{/if}} + {{/block-slot}} + {{/healthchecked-resource}} + {{/list-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

+ There are no healthy nodes for that search. +

+ {{/block-slot}} + {{/changeable-set}} +
{{/if}} {{#if (and (eq healthy.length 0) (eq unhealthy.length 0)) }} -

- There are no nodes. -

+

+ There are no nodes. +

{{/if}} - {{/block-slot}} + {{/block-slot}} {{/app-view}} \ No newline at end of file From 70f04bb645e7b0c211ba4df09f6f07ddebcd0d82 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Mon, 5 Aug 2019 13:46:46 +0000 Subject: [PATCH 8/8] ui: adds a test to to assert no errors happen with an unelected leader --- .../acceptance/dc/nodes/no-leader.feature | 18 ++++++++++++++++++ .../steps/dc/nodes/no-leader-steps.js | 10 ++++++++++ 2 files changed, 28 insertions(+) create mode 100644 ui-v2/tests/acceptance/dc/nodes/no-leader.feature create mode 100644 ui-v2/tests/acceptance/steps/dc/nodes/no-leader-steps.js diff --git a/ui-v2/tests/acceptance/dc/nodes/no-leader.feature b/ui-v2/tests/acceptance/dc/nodes/no-leader.feature new file mode 100644 index 000000000000..84f6036cfbed --- /dev/null +++ b/ui-v2/tests/acceptance/dc/nodes/no-leader.feature @@ -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 + diff --git a/ui-v2/tests/acceptance/steps/dc/nodes/no-leader-steps.js b/ui-v2/tests/acceptance/steps/dc/nodes/no-leader-steps.js new file mode 100644 index 000000000000..a7eff3228bf4 --- /dev/null +++ b/ui-v2/tests/acceptance/steps/dc/nodes/no-leader-steps.js @@ -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); + }); +}