ui: Add new Service.Mesh* properties for improved sorting (#8542)

* ui: Serialize proxies into the model, add Mesh* model props

Serializes the proxies associated with a service onto the Service model
itself, then adds various Mesh* properties

* ui: Uses the new Mesh* properties throughout the app
This commit is contained in:
John Cowen 2020-08-21 08:53:22 +01:00 committed by GitHub
parent 1c3a638d69
commit 7238089fc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 109 additions and 117 deletions

View File

@ -1,19 +1,17 @@
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-service-list" as |item index|> <ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-service-list" as |item index|>
<BlockSlot @name="header"> <BlockSlot @name="header">
{{#let (get proxies item.Name) as |proxy|}} <dl class={{item.MeshStatus}}>
{{#let (service/health-checks item proxy) as |health|}}
<dl class={{health}}>
<dt> <dt>
Health Health
</dt> </dt>
<dd> <dd>
<Tooltip @position="top-start"> <Tooltip @position="top-start">
{{#if (eq 'critical' health)}} {{#if (eq 'critical' item.MeshStatus)}}
At least one health check on one instance is failing. At least one health check on one instance is failing.
{{else if (eq 'warning' health)}} {{else if (eq 'warning' item.MeshStatus)}}
At least one health check on one instance has a warning. At least one health check on one instance has a warning.
{{else if (eq 'passing' health)}} {{else if (eq 'passing' item.MeshStatus)}}
All health checks are passing. All health checks are passing.
{{else}} {{else}}
There are no health checks. There are no health checks.
@ -21,8 +19,6 @@
</Tooltip> </Tooltip>
</dd> </dd>
</dl> </dl>
{{/let}}
{{/let}}
{{#if (gt item.InstanceCount 0)}} {{#if (gt item.InstanceCount 0)}}
{{#if (eq item.Kind 'terminating-gateway')}} {{#if (eq item.Kind 'terminating-gateway')}}
<a data-test-service-name href={{href-to "dc.services.show.services" item.Name}}> <a data-test-service-name href={{href-to "dc.services.show.services" item.Name}}>
@ -65,7 +61,7 @@
{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'Instance' without-count=true}} {{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'Instance' without-count=true}}
</span> </span>
{{/if}} {{/if}}
{{#if (get proxies item.Name)}} {{#if item.Proxy}}
<dl class="proxy"> <dl class="proxy">
<dt> <dt>
<Tooltip> <Tooltip>

View File

@ -1,17 +1,17 @@
<ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-upstream-list" as |item index|> <ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-upstream-list" as |item index|>
<BlockSlot @name="header"> <BlockSlot @name="header">
{{#if (gt item.InstanceCount 0)}} {{#if (gt item.InstanceCount 0)}}
<dl class={{service/health-checks item}}> <dl class={{item.MeshStatus}}>
<dt> <dt>
Health Health
</dt> </dt>
<dd> <dd>
<Tooltip @position="top-start"> <Tooltip @position="top-start">
{{#if (eq 'critical' (service/health-checks item))}} {{#if (eq 'critical' item.MeshStatus)}}
At least one health check on one instance is failing. At least one health check on one instance is failing.
{{else if (eq 'warning' (service/health-checks item))}} {{else if (eq 'warning' item.MeshStatus)}}
At least one health check on one instance has a warning. At least one health check on one instance has a warning.
{{else if (eq 'passing' (service/health-checks item))}} {{else if (eq 'passing' item.MeshStatus)}}
All health checks are passing. All health checks are passing.
{{else}} {{else}}
There are no health checks. There are no health checks.

View File

@ -13,22 +13,4 @@ export default Controller.extend({
return item.Kind !== 'connect-proxy'; return item.Kind !== 'connect-proxy';
}); });
}), }),
proxies: computed('items.[]', function() {
const proxies = {};
this.items
.filter(function(item) {
return item.Kind === 'connect-proxy';
})
.forEach(item => {
// Iterating to cover the usecase of a proxy being
// used by more than one service
if (item.ProxyFor) {
item.ProxyFor.forEach(service => {
proxies[service] = item;
});
}
});
return proxies;
}),
}); });

View File

@ -1,19 +0,0 @@
import { helper } from '@ember/component/helper';
export function healthChecks(
[item, proxy = { ChecksCritical: 0, ChecksWarning: 0, ChecksPassing: 0 }],
hash
) {
switch (true) {
case item.ChecksCritical !== 0 || proxy.ChecksCritical !== 0:
return 'critical';
case item.ChecksWarning !== 0 || proxy.ChecksWarning !== 0:
return 'warning';
case item.ChecksPassing !== 0 || proxy.ChecksPassing !== 0:
return 'passing';
default:
return 'empty';
}
}
export default helper(healthChecks);

View File

@ -15,6 +15,7 @@ export default Model.extend({
}), }),
InstanceCount: attr('number'), InstanceCount: attr('number'),
ProxyFor: attr(), ProxyFor: attr(),
Proxy: attr(),
Kind: attr('string'), Kind: attr('string'),
ExternalSources: attr(), ExternalSources: attr(),
GatewayConfig: attr(), GatewayConfig: attr(),
@ -37,6 +38,41 @@ export default Model.extend({
Checks: attr(), Checks: attr(),
SyncTime: attr('number'), SyncTime: attr('number'),
meta: attr(), meta: attr(),
/* Mesh properties involve both the service and the associated proxy */
MeshStatus: computed('MeshChecksPassing', 'MeshChecksWarning', 'MeshChecksCritical', function() {
switch (true) {
case this.MeshChecksCritical !== 0:
return 'critical';
case this.MeshChecksWarning !== 0:
return 'warning';
case this.MeshChecksPassing !== 0:
return 'passing';
default:
return 'empty';
}
}),
MeshChecksPassing: computed('ChecksPassing', 'Proxy.ChecksPassing', function() {
let proxyCount = 0;
if (typeof this.Proxy !== 'undefined') {
proxyCount = this.Proxy.ChecksPassing;
}
return this.ChecksPassing + proxyCount;
}),
MeshChecksWarning: computed('ChecksWarning', 'Proxy.ChecksWarning', function() {
let proxyCount = 0;
if (typeof this.Proxy !== 'undefined') {
proxyCount = this.Proxy.ChecksWarning;
}
return this.ChecksWarning + proxyCount;
}),
MeshChecksCritical: computed('ChecksCritical', 'Proxy.ChecksCritical', function() {
let proxyCount = 0;
if (typeof this.Proxy !== 'undefined') {
proxyCount = this.Proxy.ChecksCritical;
}
return this.ChecksCritical + proxyCount;
}),
/**/
passing: computed('ChecksPassing', 'Checks', function() { passing: computed('ChecksPassing', 'Checks', function() {
let num = 0; let num = 0;
// TODO: use typeof // TODO: use typeof

View File

@ -5,6 +5,41 @@ import { get } from '@ember/object';
export default Serializer.extend({ export default Serializer.extend({
primaryKey: PRIMARY_KEY, primaryKey: PRIMARY_KEY,
slugKey: SLUG_KEY, slugKey: SLUG_KEY,
respondForQuery: function(respond, query) {
return this._super(
cb =>
respond((headers, body) => {
// Services and proxies all come together in the same list
// Here we map the proxies to their related services on a Service.Proxy
// property for easy access later on
const services = {};
body
.filter(function(item) {
return item.Kind !== 'connect-proxy';
})
.forEach(item => {
services[item.Name] = item;
});
body
.filter(function(item) {
return item.Kind === 'connect-proxy';
})
.forEach(item => {
// Iterating to cover the usecase of a proxy being
// used by more than one service
if (item.ProxyFor) {
item.ProxyFor.forEach(service => {
if (typeof services[service] !== 'undefined') {
services[service].Proxy = item;
}
});
}
});
return cb(headers, body);
}),
query
);
},
respondForQueryRecord: function(respond, query) { respondForQueryRecord: function(respond, query) {
// Name is added here from the query, which is used to make the uid // Name is added here from the query, which is used to make the uid
// Datacenter gets added in the ApplicationSerializer // Datacenter gets added in the ApplicationSerializer

View File

@ -11,21 +11,21 @@ export default () => key => {
b = serviceB; b = serviceB;
} }
switch (true) { switch (true) {
case a.ChecksCritical > b.ChecksCritical: case a.MeshChecksCritical > b.MeshChecksCritical:
return 1; return 1;
case a.ChecksCritical < b.ChecksCritical: case a.MeshChecksCritical < b.MeshChecksCritical:
return -1; return -1;
default: default:
switch (true) { switch (true) {
case a.ChecksWarning > b.ChecksWarning: case a.MeshChecksWarning > b.MeshChecksWarning:
return 1; return 1;
case a.ChecksWarning < b.ChecksWarning: case a.MeshChecksWarning < b.MeshChecksWarning:
return -1; return -1;
default: default:
switch (true) { switch (true) {
case a.ChecksPassing < b.ChecksPassing: case a.MeshChecksPassing < b.MeshChecksPassing:
return 1; return 1;
case a.ChecksPassing > b.ChecksPassing: case a.MeshChecksPassing > b.MeshChecksPassing:
return -1; return -1;
} }
} }

View File

@ -59,7 +59,7 @@
{{#let (sort-by (comparator 'service' sort) services) as |sorted|}} {{#let (sort-by (comparator 'service' sort) services) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'service' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'service' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |filtered|>
<ConsulServiceList @items={{filtered}} @proxies={{proxies}}/> <ConsulServiceList @items={{filtered}}/>
</BlockSlot> </BlockSlot>
<BlockSlot @name="empty"> <BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}> <EmptyState @allowLogin={{true}}>

View File

@ -37,7 +37,9 @@ module('Integration | Serializer | service', function(hooks) {
ns: nspace, ns: nspace,
} }
); );
assert.deepEqual(actual, expected); assert.equal(actual[0].Namespace, expected[0].Namespace);
assert.equal(actual[0].Datacenter, expected[0].Datacenter);
assert.equal(actual[0].uid, expected[0].uid);
}); });
}); });
test(`respondForQuery returns the correct data for list endpoint when gateway is set when nspace is ${nspace}`, function(assert) { test(`respondForQuery returns the correct data for list endpoint when gateway is set when nspace is ${nspace}`, function(assert) {

View File

@ -1,5 +1,4 @@
import { moduleFor, test } from 'ember-qunit'; import { moduleFor, test } from 'ember-qunit';
import { skip } from 'qunit';
import repo from 'consul-ui/tests/helpers/repo'; import repo from 'consul-ui/tests/helpers/repo';
import { get } from '@ember/object'; import { get } from '@ember/object';
const NAME = 'service'; const NAME = 'service';
@ -7,7 +6,6 @@ moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, {
// Specify the other units that are required for this test. // Specify the other units that are required for this test.
integration: true, integration: true,
}); });
skip('findBySlug returns a sane tree');
const dc = 'dc-1'; const dc = 'dc-1';
const id = 'token-name'; const id = 'token-name';
const now = new Date().getTime(); const now = new Date().getTime();
@ -41,44 +39,6 @@ const undefinedNspace = 'default';
}); });
}); });
test(`findByDatacenter returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) {
get(this.subject(), 'store').serializerFor(NAME).timestamp = function() {
return now;
};
return repo(
'Service',
'findAllByDatacenter',
this.subject(),
function retrieveStub(stub) {
return stub(
`/v1/internal/ui/services?dc=${dc}${
typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``
}`,
{
CONSUL_SERVICE_COUNT: '100',
}
);
},
function performTest(service) {
return service.findAllByDatacenter(dc, nspace || undefinedNspace);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(item =>
Object.assign({}, item, {
SyncTime: now,
Datacenter: dc,
Namespace: item.Namespace || undefinedNspace,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`,
})
);
})
);
}
);
});
test(`findBySlug returns the correct data for item endpoint when the nspace is ${nspace}`, function(assert) { test(`findBySlug returns the correct data for item endpoint when the nspace is ${nspace}`, function(assert) {
return repo( return repo(
'Service', 'Service',

View File

@ -8,22 +8,22 @@ module('Unit | Sort | Comparator | service', function() {
const actual = comparator(expected); const actual = comparator(expected);
assert.equal(actual, expected); assert.equal(actual, expected);
}); });
test('items are sorted by a fake Status which uses Checks{Passing,Warning,Critical}', function(assert) { test('items are sorted by a fake Status which uses MeshChecks{Passing,Warning,Critical}', function(assert) {
const items = [ const items = [
{ {
ChecksPassing: 1, MeshChecksPassing: 1,
ChecksWarning: 1, MeshChecksWarning: 1,
ChecksCritical: 1, MeshChecksCritical: 1,
}, },
{ {
ChecksPassing: 1, MeshChecksPassing: 1,
ChecksWarning: 1, MeshChecksWarning: 1,
ChecksCritical: 2, MeshChecksCritical: 2,
}, },
{ {
ChecksPassing: 1, MeshChecksPassing: 1,
ChecksWarning: 1, MeshChecksWarning: 1,
ChecksCritical: 3, MeshChecksCritical: 3,
}, },
]; ];
const comp = comparator('Status:asc'); const comp = comparator('Status:asc');
@ -31,19 +31,19 @@ module('Unit | Sort | Comparator | service', function() {
const expected = [ const expected = [
{ {
ChecksPassing: 1, MeshChecksPassing: 1,
ChecksWarning: 1, MeshChecksWarning: 1,
ChecksCritical: 3, MeshChecksCritical: 3,
}, },
{ {
ChecksPassing: 1, MeshChecksPassing: 1,
ChecksWarning: 1, MeshChecksWarning: 1,
ChecksCritical: 2, MeshChecksCritical: 2,
}, },
{ {
ChecksPassing: 1, MeshChecksPassing: 1,
ChecksWarning: 1, MeshChecksWarning: 1,
ChecksCritical: 1, MeshChecksCritical: 1,
}, },
]; ];
let actual = items.sort(comp); let actual = items.sort(comp);