ui: Add consul-service-list presentational component (#7279)

This commit moves our service list into a new presentational component,
and is therefore mainly just moving things around. The main thing moved
here is the logic required to resizing columns correctly is now moved to
a component instead of the controller
This commit is contained in:
John Cowen 2020-02-12 17:46:29 +00:00 committed by John Cowen
parent 7dc5201676
commit 771973a713
5 changed files with 127 additions and 92 deletions

View File

@ -0,0 +1,65 @@
import Component from '@ember/component';
import { get, computed } from '@ember/object';
import { htmlSafe } from '@ember/string';
const max = function(arr, prop) {
return arr.reduce(function(prev, item) {
return Math.max(prev, get(item, prop));
}, 0);
};
const chunk = function(str, size) {
const num = Math.ceil(str.length / size);
const chunks = new Array(num);
for (let i = 0, o = 0; i < num; ++i, o += size) {
chunks[i] = str.substr(o, size);
}
return chunks;
};
const width = function(num) {
const str = num.toString();
const len = str.length;
const commas = chunk(str, 3).length - 1;
return commas * 4 + len * 10;
};
const widthDeclaration = function(num) {
return htmlSafe(`width: ${num}px`);
};
export default Component.extend({
tagName: '',
onchange: function() {},
maxWidth: computed('{maxPassing,maxWarning,maxCritical}', function() {
const PADDING = 32 * 3 + 13;
return ['maxPassing', 'maxWarning', 'maxCritical'].reduce((prev, item) => {
return prev + width(get(this, item));
}, PADDING);
}),
totalWidth: computed('maxWidth', function() {
return widthDeclaration(get(this, 'maxWidth'));
}),
remainingWidth: computed('maxWidth', function() {
// maxWidth is the maximum width of the healthchecks column
// there are currently 2 other columns so divide it by 2 and
// take that off 50% (100% / number of fluid columns)
// also we added a Type column which we've currently fixed to 100px
// so again divide that by 2 and take it off each fluid column
return htmlSafe(`width: calc(50% - 50px - ${Math.round(get(this, 'maxWidth') / 2)}px)`);
}),
maxPassing: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksPassing');
}),
maxWarning: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksWarning');
}),
maxCritical: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksCritical');
}),
passingWidth: computed('maxPassing', function() {
return widthDeclaration(width(get(this, 'maxPassing')));
}),
warningWidth: computed('maxWarning', function() {
return widthDeclaration(width(get(this, 'maxWarning')));
}),
criticalWidth: computed('maxCritical', function() {
return widthDeclaration(width(get(this, 'maxCritical')));
}),
});

View File

@ -1,30 +1,7 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { get, computed } from '@ember/object'; import { get, computed } from '@ember/object';
import { htmlSafe } from '@ember/string';
import WithEventSource from 'consul-ui/mixins/with-event-source'; import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithSearching from 'consul-ui/mixins/with-searching'; import WithSearching from 'consul-ui/mixins/with-searching';
const max = function(arr, prop) {
return arr.reduce(function(prev, item) {
return Math.max(prev, get(item, prop));
}, 0);
};
const chunk = function(str, size) {
const num = Math.ceil(str.length / size);
const chunks = new Array(num);
for (let i = 0, o = 0; i < num; ++i, o += size) {
chunks[i] = str.substr(o, size);
}
return chunks;
};
const width = function(num) {
const str = num.toString();
const len = str.length;
const commas = chunk(str, 3).length - 1;
return commas * 4 + len * 10;
};
const widthDeclaration = function(num) {
return htmlSafe(`width: ${num}px`);
};
export default Controller.extend(WithEventSource, WithSearching, { export default Controller.extend(WithEventSource, WithSearching, {
queryParams: { queryParams: {
s: { s: {
@ -42,39 +19,4 @@ export default Controller.extend(WithEventSource, WithSearching, {
.add(this.items) .add(this.items)
.search(this.terms); .search(this.terms);
}), }),
maxWidth: computed('{maxPassing,maxWarning,maxCritical}', function() {
const PADDING = 32 * 3 + 13;
return ['maxPassing', 'maxWarning', 'maxCritical'].reduce((prev, item) => {
return prev + width(get(this, item));
}, PADDING);
}),
totalWidth: computed('maxWidth', function() {
return widthDeclaration(this.maxWidth);
}),
remainingWidth: computed('maxWidth', function() {
// maxWidth is the maximum width of the healthchecks column
// there are currently 2 other columns so divide it by 2 and
// take that off 50% (100% / number of fluid columns)
// also we added a Type column which we've currently fixed to 100px
// so again divide that by 2 and take it off each fluid column
return htmlSafe(`width: calc(50% - ${Math.round(this.maxWidth / 2)}px)`);
}),
maxPassing: computed('items.[]', function() {
return max(this.items, 'ChecksPassing');
}),
maxWarning: computed('items.[]', function() {
return max(this.items, 'ChecksWarning');
}),
maxCritical: computed('items.[]', function() {
return max(this.items, 'ChecksCritical');
}),
passingWidth: computed('maxPassing', function() {
return widthDeclaration(width(this.maxPassing));
}),
warningWidth: computed('maxWarning', function() {
return widthDeclaration(width(this.maxWarning));
}),
criticalWidth: computed('maxCritical', function() {
return widthDeclaration(width(this.maxCritical));
}),
}); });

View File

@ -0,0 +1,37 @@
{{#if (gt items.length 0)}}
{{#tabular-collection items=items as |item index|}}
{{#block-slot name='header'}}
<th style={{remainingWidth}}>Service</th>
<th style={{totalWidth}}>
Health Checks
<span>
<em role="tooltip">The number of health checks for the service on all nodes</em>
</span>
</th>
<th style={{remainingWidth}}>Tags</th>
{{/block-slot}}
{{#block-slot name='row'}}
<td data-test-service={{item.Name}} style={{remainingWidth}}>
<a href={{href-to routeName item.Name}}>
{{#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.Name}}
</a>
</td>
<td style={{totalWidth}}>
{{healthcheck-info
passing=item.ChecksPassing warning=item.ChecksWarning critical=item.ChecksCritical
passingWidth=passingWidth warningWidth=warningWidth criticalWidth=criticalWidth
}}
</td>
<td style={{remainingWidth}}>
{{tag-list items=item.Tags}}
</td>
{{/block-slot}}
{{/tabular-collection}}
{{/if}}

View File

@ -22,40 +22,7 @@
{{#block-slot name='content'}} {{#block-slot name='content'}}
{{#changeable-set dispatcher=searchable}} {{#changeable-set dispatcher=searchable}}
{{#block-slot name='set' as |filtered|}} {{#block-slot name='set' as |filtered|}}
{{#tabular-collection {{consul-service-list routeName="dc.services.show" items=filtered}}
route='dc.services.show'
key='Name'
items=filtered as |item index|
}}
{{#block-slot name='header'}}
<th style={{remainingWidth}}>Service</th>
<th style={{totalWidth}}>Health Checks<span><em role="tooltip">The number of health checks for the service on all nodes</em></span></th>
<th style={{remainingWidth}}>Tags</th>
{{/block-slot}}
{{#block-slot name='row'}}
<td data-test-service="{{item.Name}}" style={{remainingWidth}}>
<a href={{href-to 'dc.services.show' item.Name}}>
{{#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.Name}}
</a>
</td>
<td style={{totalWidth}}>
{{healthcheck-info
passing=item.ChecksPassing warning=item.ChecksWarning critical=item.ChecksCritical
passingWidth=passingWidth warningWidth=warningWidth criticalWidth=criticalWidth
}}
</td>
<td style={{remainingWidth}}>
{{tag-list items=item.Tags}}
</td>
{{/block-slot}}
{{/tabular-collection}}
{{/block-slot}} {{/block-slot}}
{{#block-slot name='empty'}} {{#block-slot name='empty'}}
<p> <p>

View File

@ -0,0 +1,24 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | consul-service-list', 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`{{consul-service-list}}`);
assert.equal(this.element.textContent.trim(), '');
// Template block usage:
await render(hbs`
{{#consul-service-list}}{{/consul-service-list}}
`);
assert.equal(this.element.textContent.trim(), '');
});
});