ui: Redesign - Node service instances tab (#8204)

* Upgrade consul-api-dobule to version 3.1.3

* Create ConsulInstaceChecks component with test

* Redesign: Service Instaces tab in for a Node

* Update Node tests to work with the ConsulServiceInstancesList

* Style fix to the copy button in the composite-row details

* Delete helper and move logic to ConsulInstanceChecks component

* Delete unused component consul-node-service-list
This commit is contained in:
Kenia 2020-07-01 10:27:29 -04:00 committed by GitHub
parent a4fe092e7a
commit b0ecfc4109
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 183 additions and 187 deletions

View File

@ -0,0 +1,32 @@
{{#if (gt items.length 0)}}
{{#if (eq healthCheck.check 'empty') }}
<dl class={{healthCheck.check}}>
<dt>
<Tooltip>
{{capitalize type}} Checks
</Tooltip>
</dt>
<dd>No {{type}} checks</dd>
</dl>
{{else}}
{{#if (eq healthCheck.count items.length)}}
<dl class={{healthCheck.check}}>
<dt>
<Tooltip>
{{capitalize type}} Checks
</Tooltip>
</dt>
<dd>All {{type}} checks {{healthCheck.status}}</dd>
</dl>
{{else}}
<dl class={{healthCheck.check}}>
<dt>
<Tooltip>
{{capitalize type}} Checks
</Tooltip>
</dt>
<dd>{{healthCheck.count}}/{{items.length}} {{type}} checks {{healthCheck.status}}</dd>
</dl>
{{/if}}
{{/if}}
{{/if}}

View File

@ -0,0 +1,52 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
tagName: '',
healthCheck: computed('items.[]', function() {
let ChecksCritical = 0;
let ChecksWarning = 0;
let ChecksPassing = 0;
this.items.forEach(item => {
switch (item.Status) {
case 'critical':
ChecksCritical += 1;
break;
case 'warning':
ChecksWarning += 1;
break;
case 'passing':
ChecksPassing += 1;
break;
default:
break;
}
});
switch (true) {
case ChecksCritical !== 0:
return {
check: 'critical',
status: 'failing',
count: ChecksCritical,
};
case ChecksWarning !== 0:
return {
check: 'warning',
status: 'with warning',
count: ChecksWarning,
};
case ChecksPassing !== 0:
return {
check: 'passing',
status: 'passing',
count: ChecksPassing,
};
default:
return {
check: 'empty',
};
}
}),
});

View File

@ -1,80 +1,25 @@
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<ListCollection @items={{items}} class="consul-service-instance-list" as |item index|> <ListCollection @items={{items}} class="consul-service-instance-list" as |item index|>
<BlockSlot @name="header"> <BlockSlot @name="header">
<a href={{href-to routeName item.Service.Service item.Node.Node (or item.Service.ID item.Service.Service)}}> {{#if (eq routeName "dc.services.show")}}
<a data-test-service-name href={{href-to routeName item.Service}}>
{{item.ID}}
</a>
{{else}}
<a data-test-service-name href={{href-to routeName item.Service.Service item.Node.Node (or item.Service.ID item.Service.Service)}}>
{{item.Service.ID}} {{item.Service.ID}}
</a> </a>
{{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="details"> <BlockSlot @name="details">
{{#if checks}}
<ConsulExternalSource @item={{item}} />
<ConsulInstanceChecks @type="service" @items={{get checks item.Service}} />
{{else}}
<ConsulExternalSource @item={{item.Service}} /> <ConsulExternalSource @item={{item.Service}} />
{{#let (reject-by 'ServiceID' '' item.Checks) as |checks|}} <ConsulInstanceChecks @type="service" @items={{reject-by 'ServiceID' '' item.Checks}} />
{{#let (service/instance-checks checks) as |serviceCheck| }} <ConsulInstanceChecks @type="node" @items={{filter-by 'ServiceID' '' item.Checks}} />
{{#if (eq serviceCheck.check 'empty') }}
<dl class={{serviceCheck.check}}>
<dt>
<Tooltip>
Service Checks
</Tooltip>
</dt>
<dd>No service checks</dd>
</dl>
{{else}}
{{#if (eq serviceCheck.count checks.length)}}
<dl class={{serviceCheck.check}}>
<dt>
<Tooltip>
Service Checks
</Tooltip>
</dt>
<dd>All service checks {{serviceCheck.status}}</dd>
</dl>
{{else}}
<dl class={{serviceCheck.check}}>
<dt>
<Tooltip>
Service Checks
</Tooltip>
</dt>
<dd>{{serviceCheck.count}}/{{checks.length}} service checks {{serviceCheck.status}}</dd>
</dl>
{{/if}} {{/if}}
{{/if}}
{{/let}}
{{/let}}
{{#let (filter-by 'ServiceID' '' item.Checks) as |checks|}}
{{#let (service/instance-checks checks) as |nodeCheck| }}
{{#if (eq nodeCheck.check 'empty') }}
<dl class={{nodeCheck.check}}>
<dt>
<Tooltip>
Node Checks
</Tooltip>
</dt>
<dd>No node checks</dd>
</dl>
{{else}}
{{#if (eq nodeCheck.count checks.length)}}
<dl class={{nodeCheck.check}}>
<dt>
<Tooltip>
Node Checks
</Tooltip>
</dt>
<dd>All node checks {{nodeCheck.status}}</dd>
</dl>
{{else}}
<dl class={{nodeCheck.check}}>
<dt>
<Tooltip>
Node Checks
</Tooltip>
</dt>
<dd>{{nodeCheck.count}}/{{checks.length}} node checks {{nodeCheck.status}}</dd>
</dl>
{{/if}}
{{/if}}
{{/let}}
{{/let}}
{{#if (get proxies item.Service.ID)}} {{#if (get proxies item.Service.ID)}}
<dl class="proxy"> <dl class="proxy">
<dt> <dt>
@ -99,6 +44,7 @@
</dd> </dd>
</dl> </dl>
{{/if}} {{/if}}
{{#if item.Service.Port}}
<dl class="address" data-test-address> <dl class="address" data-test-address>
<dt> <dt>
<Tooltip> <Tooltip>
@ -113,7 +59,23 @@
{{/if}} {{/if}}
</dd> </dd>
</dl> </dl>
{{/if}}
{{#if (and checks item.Port)}}
<dl>
<dt>
<CopyButton
@value={{item.Port}}
@name="Port"
/>
</dt>
<dd data-test-service-port={{item.Port}}>:{{item.Port}}</dd>
</dl>
{{/if}}
{{#if checks}}
<TagList @item={{item}} />
{{else}}
<TagList @item={{item.Service}} /> <TagList @item={{item.Service}} />
{{/if}}
</BlockSlot> </BlockSlot>
</ListCollection> </ListCollection>
{{/if}} {{/if}}

View File

@ -1,5 +1,6 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { alias } from '@ember/object/computed'; import { alias } from '@ember/object/computed';
import { get, computed } from '@ember/object';
export default Controller.extend({ export default Controller.extend({
items: alias('item.Services'), items: alias('item.Services'),
@ -9,4 +10,19 @@ export default Controller.extend({
replace: true, replace: true,
}, },
}, },
checks: computed('item.Checks.[]', function() {
const checks = {};
get(this, 'item.Checks')
.filter(item => {
return item.ServiceID !== '';
})
.forEach(item => {
if (typeof checks[item.ServiceID] === 'undefined') {
checks[item.ServiceID] = [];
}
checks[item.ServiceID].push(item);
});
return checks;
}),
}); });

View File

@ -1,50 +0,0 @@
import { helper } from '@ember/component/helper';
export function healthChecks([items], hash) {
let ChecksCritical = 0;
let ChecksWarning = 0;
let ChecksPassing = 0;
items.forEach(item => {
switch (item.Status) {
case 'critical':
ChecksCritical += 1;
break;
case 'warning':
ChecksWarning += 1;
break;
case 'passing':
ChecksPassing += 1;
break;
default:
break;
}
});
switch (true) {
case ChecksCritical !== 0:
return {
check: 'critical',
status: 'failing',
count: ChecksCritical,
};
case ChecksWarning !== 0:
return {
check: 'warning',
status: 'with warning',
count: ChecksWarning,
};
case ChecksPassing !== 0:
return {
check: 'passing',
status: 'passing',
count: ChecksPassing,
};
default:
return {
check: 'empty',
};
}
}
export default helper(healthChecks);

View File

@ -67,7 +67,7 @@ export const routes = {
_options: { path: '/health-checks' }, _options: { path: '/health-checks' },
}, },
services: { services: {
_options: { path: '/services' }, _options: { path: '/service-instances' },
}, },
rtt: { rtt: {
_options: { path: '/round-trip-time' }, _options: { path: '/round-trip-time' },

View File

@ -73,6 +73,7 @@
} }
%composite-row-detail .copy-button { %composite-row-detail .copy-button {
margin-right: 4px; margin-right: 4px;
margin-top: 2px;
} }
%composite-row-header .copy-button { %composite-row-header .copy-button {
margin-left: 4px; margin-left: 4px;

View File

@ -21,7 +21,7 @@
compact compact
(array (array
(hash label="Health Checks" href=(href-to "dc.nodes.show.healthchecks") selected=(is-href "dc.nodes.show.healthchecks")) (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")) (hash label="Service Instances" 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")) '') (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="Lock Sessions" href=(href-to "dc.nodes.show.sessions") selected=(is-href "dc.nodes.show.sessions"))
(hash label="Metadata" href=(href-to "dc.nodes.show.metadata") selected=(is-href "dc.nodes.show.metadata")) (hash label="Metadata" href=(href-to "dc.nodes.show.metadata") selected=(is-href "dc.nodes.show.metadata"))

View File

@ -10,36 +10,7 @@
{{/if}} {{/if}}
<ChangeableSet @dispatcher={{searchable 'nodeservice' items}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'nodeservice' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |filtered|>
<TabularCollection <ConsulServiceInstanceList @routeName="dc.services.show" @items={{filtered}} @checks={{checks}}/>
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 @item={{item}} />
</td>
</BlockSlot>
</TabularCollection>
</BlockSlot> </BlockSlot>
<BlockSlot @name="empty"> <BlockSlot @name="empty">
<p> <p>

View File

@ -90,8 +90,8 @@ Feature: components / catalog-filter
node: node-0 node: node-0
--- ---
# And I see 3 healthcheck model with the name "Disk Util" # And I see 3 healthcheck model with the name "Disk Util"
When I click services on the tabs When I click serviceInstances on the tabs
And I see servicesIsSelected on the tabs And I see serviceInstancesIsSelected on the tabs
Then I fill in with yaml Then I fill in with yaml
--- ---
@ -101,12 +101,6 @@ Feature: components / catalog-filter
And I see 1 [Model] model with the port "65535" And I see 1 [Model] model with the port "65535"
Then I fill in with yaml Then I fill in with yaml
--- ---
s: service-0-with-id
---
And I see 1 [Model] model
And I see 1 [Model] model with the id "service-0-with-id"
Then I fill in with yaml
---
s: hard drive s: hard drive
--- ---
And I see 1 [Model] model with the name "[Model]-1" And I see 1 [Model] model with the name "[Model]-1"

View File

@ -30,24 +30,18 @@ Feature: dc / nodes / services / list: Node > Services Listing
Tags: [] Tags: []
Meta: Meta:
external-source: kubernetes external-source: kubernetes
- ID: 'service-4'
Port: 3
Service: 'service-4'
Tags: []
Meta: ~
--- ---
When I visit the node page for yaml When I visit the node page for yaml
--- ---
dc: dc1 dc: dc1
node: node-0 node: node-0
--- ---
When I click services on the tabs When I click serviceInstances on the tabs
And I see servicesIsSelected on the tabs And I see serviceInstancesIsSelected on the tabs
And I see externalSource on the services like yaml And I see externalSource on the services like yaml
--- ---
- consul - consul
- nomad - nomad
- terraform - terraform
- kubernetes - kubernetes
- ~
--- ---

View File

@ -11,8 +11,8 @@ Feature: dc / nodes / show: Show node
--- ---
And I see healthChecksIsSelected on the tabs And I see healthChecksIsSelected on the tabs
When I click services on the tabs When I click serviceInstances on the tabs
And I see servicesIsSelected on the tabs And I see serviceInstancesIsSelected on the tabs
When I click roundTripTime on the tabs When I click roundTripTime on the tabs
And I see roundTripTimeIsSelected on the tabs And I see roundTripTimeIsSelected on the tabs
@ -34,14 +34,14 @@ Feature: dc / nodes / show: Show node
--- ---
And I see healthChecksIsSelected on the tabs And I see healthChecksIsSelected on the tabs
When I click services on the tabs When I click serviceInstances on the tabs
And I see servicesIsSelected on the tabs And I see serviceInstancesIsSelected on the tabs
And I don't see roundTripTime on the tabs And I don't see roundTripTime on the tabs
When I click lockSessions on the tabs When I click lockSessions on the tabs
And I see lockSessionsIsSelected on the tabs And I see lockSessionsIsSelected on the tabs
Scenario: Given 1 node with no checks all the tabs are visible but the Services tab is selected Scenario: Given 1 node with no checks all the tabs are visible but the serviceInstances tab is selected
Given 1 node models from yaml Given 1 node models from yaml
--- ---
ID: node-0 ID: node-0
@ -53,10 +53,10 @@ Feature: dc / nodes / show: Show node
node: node-0 node: node-0
--- ---
And I see healthChecks on the tabs And I see healthChecks on the tabs
And I see services on the tabs And I see serviceInstances on the tabs
And I don't see roundTripTime on the tabs And I don't see roundTripTime on the tabs
And I see lockSessions on the tabs And I see lockSessions on the tabs
And I see servicesIsSelected on the tabs And I see serviceInstancesIsSelected on the tabs
Scenario: A node warns when deregistered whilst blocking Scenario: A node warns when deregistered whilst blocking
Given 1 node model from yaml Given 1 node model from yaml
--- ---

View File

@ -0,0 +1,25 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | consul-instance-checks', function(hooks) {
setupRenderingTest(hooks);
test('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`<ConsulInstanceChecks />`);
assert.equal(this.element.textContent.trim(), '');
// Template block usage:
await render(hbs`
<ConsulInstanceChecks>
</ConsulInstanceChecks>
`);
assert.equal(this.element.textContent.trim(), '');
});
});

View File

@ -138,7 +138,7 @@ export default {
), ),
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, clickable, attribute, collection, catalogFilter)),
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup)), node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup, text)),
kvs: create(kvs(visitable, deletable, creatable, clickable, attribute, collection)), kvs: create(kvs(visitable, deletable, creatable, clickable, attribute, collection)),
kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)), kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)),
acls: create(acls(visitable, deletable, creatable, clickable, attribute, collection, aclFilter)), acls: create(acls(visitable, deletable, creatable, clickable, attribute, collection, aclFilter)),

View File

@ -1,9 +1,9 @@
export default function(visitable, deletable, clickable, attribute, collection, tabs) { export default function(visitable, deletable, clickable, attribute, collection, tabs, text) {
return { return {
visit: visitable('/:dc/nodes/:node'), visit: visitable('/:dc/nodes/:node'),
tabs: tabs('tab', [ tabs: tabs('tab', [
'health-checks', 'health-checks',
'services', 'service-instances',
'round-trip-time', 'round-trip-time',
'lock-sessions', 'lock-sessions',
'metadata', 'metadata',
@ -11,11 +11,10 @@ export default function(visitable, deletable, clickable, attribute, collection,
healthchecks: collection('[data-test-node-healthcheck]', { healthchecks: collection('[data-test-node-healthcheck]', {
name: attribute('data-test-node-healthcheck'), name: attribute('data-test-node-healthcheck'),
}), }),
services: collection('#services [data-test-tabular-row]', { services: collection('.consul-service-instance-list > ul > li:not(:first-child)', {
id: attribute('data-test-service-id', '[data-test-service-id]'), name: text('[data-test-service-name]'),
name: attribute('data-test-service-name', '[data-test-service-name]'), port: attribute('data-test-service-port', '[data-test-service-port]'),
port: attribute('data-test-service-port', '.port'), externalSource: attribute('data-test-external-source', '[data-test-external-source]'),
externalSource: attribute('data-test-external-source', 'a span'),
}), }),
sessions: collection( sessions: collection(
'#lock-sessions [data-test-tabular-row]', '#lock-sessions [data-test-tabular-row]',

View File

@ -1211,9 +1211,9 @@
js-yaml "^3.13.1" js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^3.0.0": "@hashicorp/consul-api-double@^3.0.0":
version "3.1.2" version "3.1.3"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-3.1.2.tgz#3c3b929ab0f8aff5f503728337caf1c1a41171fb" resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-3.1.3.tgz#62f8780c8513e9b37f29302543c29143b4024141"
integrity sha512-igs6f9fiA+z2Us1oLZ49/sEU0WsL+s7a1pnwFtED2xdI8tn5hz9G0doYfOxmi04IifNxv80NVifl3rZl2rn2tw== integrity sha512-IZ90RK8g4/QPxQpRLnatwpBQh9Z3kQJjOGiUVz+CrSlXg4KRLhQCFFz/gI2vmhAXRACyTxIWuydPV6BcN4ptZA==
"@hashicorp/ember-cli-api-double@^3.1.0": "@hashicorp/ember-cli-api-double@^3.1.0":
version "3.1.0" version "3.1.0"