ui: Redesign Service List page (#7605)
* Create GridCollection for nodes page with styling * Update ListCollection styling * Update TagList styling * Create CompositeRow styling component * Update ConsulServiceList component with styling * Create service health-checks helper * Add InstanceCount to the service model * Add tag-svg to codebase * Create and update tests for service-list page * Upgrade @hashicorp/consul-api-double to 2.14.0
This commit is contained in:
parent
e34c16a90c
commit
d0c4312923
|
@ -1,34 +1,15 @@
|
|||
{{yield}}
|
||||
{{#if (gt items.length 0)}}
|
||||
<TabularCollection @items={{items}} as |item index|>
|
||||
<BlockSlot @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>
|
||||
</BlockSlot>
|
||||
<BlockSlot @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}}>
|
||||
<HealthcheckInfo @passing={{item.ChecksPassing}} @warning={{item.ChecksWarning}} @critical={{item.ChecksCritical}} @passingWidth={{passingWidth}} @warningWidth={{warningWidth}} @criticalWidth={{criticalWidth}} />
|
||||
</td>
|
||||
<td style={{remainingWidth}}>
|
||||
<TagList @items={{item.Tags}} />
|
||||
</td>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
<ListCollection @cellHeight={{73}} @items={{items}} class="consul-service-list" as |item index|>
|
||||
<a href={{href-to routeName item.Name}}>
|
||||
<span class={{service/health-checks item}}></span>
|
||||
<span>
|
||||
{{item.Name}}
|
||||
</span>
|
||||
<span data-test-external-source="{{service/external-source item}}" class={{service/external-source item}}></span>
|
||||
<YieldSlot @name="metadata" @params={{block-params item}}>
|
||||
{{yield}}
|
||||
</YieldSlot>
|
||||
</a>
|
||||
</ListCollection>
|
||||
{{/if}}
|
|
@ -1,65 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import { get, computed } from '@ember/object';
|
||||
import { htmlSafe } from '@ember/string';
|
||||
import Slotted from 'block-slots';
|
||||
|
||||
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({
|
||||
export default Component.extend(Slotted, {
|
||||
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')));
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<EmberNativeScrollable @tagName="ul" @content-size={{_contentSize}} @scroll-left={{_scrollLeft}} @scroll-top={{_scrollTop}} @scrollChange={{action "scrollChange"}} @clientSizeChange={{action "clientSizeChange"}}>
|
||||
<li></li>
|
||||
{{~#each _cells as |cell|~}}
|
||||
<li style={{{cell.style}}}>{{yield cell.item cell.index }}</li>
|
||||
{{~/each~}}
|
||||
</EmberNativeScrollable>
|
|
@ -0,0 +1,78 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { computed, get, set } from '@ember/object';
|
||||
import Component from 'ember-collection/components/ember-collection';
|
||||
import PercentageColumns from 'ember-collection/layouts/percentage-columns';
|
||||
import style from 'ember-computed-style';
|
||||
import WithResizing from 'consul-ui/mixins/with-resizing';
|
||||
|
||||
export default Component.extend(WithResizing, {
|
||||
dom: service('dom'),
|
||||
tagName: 'div',
|
||||
attributeBindings: ['style'],
|
||||
height: 500,
|
||||
cellHeight: 113,
|
||||
style: style('getStyle'),
|
||||
classNames: ['grid-collection'],
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.columns = [25, 25, 25, 25];
|
||||
},
|
||||
didReceiveAttrs: function() {
|
||||
this._super(...arguments);
|
||||
this._cellLayout = this['cell-layout'] = new PercentageColumns(
|
||||
get(this, 'items.length'),
|
||||
get(this, 'columns'),
|
||||
get(this, 'cellHeight')
|
||||
);
|
||||
},
|
||||
getStyle: computed('height', function() {
|
||||
return {
|
||||
height: get(this, 'height'),
|
||||
};
|
||||
}),
|
||||
resize: function(e) {
|
||||
// TODO: This top part is very similar to resize in tabular-collection
|
||||
// see if it make sense to DRY out
|
||||
const dom = get(this, 'dom');
|
||||
const $appContent = dom.element('main > div');
|
||||
if ($appContent) {
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const $footer = dom.element('footer[role="contentinfo"]');
|
||||
const space = rect.top + $footer.clientHeight;
|
||||
const height = e.detail.height - space;
|
||||
this.set('height', Math.max(0, height));
|
||||
this.updateItems();
|
||||
this.updateScrollPosition();
|
||||
}
|
||||
const width = e.detail.width;
|
||||
const len = get(this, 'columns.length');
|
||||
switch (true) {
|
||||
case width > 1013:
|
||||
if (len != 4) {
|
||||
set(this, 'columns', [25, 25, 25, 25]);
|
||||
}
|
||||
break;
|
||||
case width > 744:
|
||||
if (len != 3) {
|
||||
set(this, 'columns', [33, 33, 34]);
|
||||
}
|
||||
break;
|
||||
case width > 487:
|
||||
if (len != 2) {
|
||||
set(this, 'columns', [50, 50]);
|
||||
}
|
||||
break;
|
||||
case width < 488:
|
||||
if (len != 1) {
|
||||
set(this, 'columns', [100]);
|
||||
}
|
||||
}
|
||||
if (len !== get(this, 'columns.length')) {
|
||||
this._cellLayout = this['cell-layout'] = new PercentageColumns(
|
||||
get(this, 'items.length'),
|
||||
get(this, 'columns'),
|
||||
get(this, 'cellHeight')
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { computed, get, set } from '@ember/object';
|
||||
import { computed, get } from '@ember/object';
|
||||
import Component from 'ember-collection/components/ember-collection';
|
||||
import PercentageColumns from 'ember-collection/layouts/percentage-columns';
|
||||
import style from 'ember-computed-style';
|
||||
|
@ -10,12 +10,11 @@ export default Component.extend(WithResizing, {
|
|||
tagName: 'div',
|
||||
attributeBindings: ['style'],
|
||||
height: 500,
|
||||
cellHeight: 113,
|
||||
style: style('getStyle'),
|
||||
classNames: ['list-collection'],
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.columns = [25, 25, 25, 25];
|
||||
this.columns = [100];
|
||||
},
|
||||
didReceiveAttrs: function() {
|
||||
this._super(...arguments);
|
||||
|
@ -36,43 +35,14 @@ export default Component.extend(WithResizing, {
|
|||
const dom = get(this, 'dom');
|
||||
const $appContent = dom.element('main > div');
|
||||
if ($appContent) {
|
||||
const border = 1;
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const $footer = dom.element('footer[role="contentinfo"]');
|
||||
const space = rect.top + $footer.clientHeight;
|
||||
const space = rect.top + $footer.clientHeight + border;
|
||||
const height = e.detail.height - space;
|
||||
this.set('height', Math.max(0, height));
|
||||
this.updateItems();
|
||||
this.updateScrollPosition();
|
||||
}
|
||||
const width = e.detail.width;
|
||||
const len = get(this, 'columns.length');
|
||||
switch (true) {
|
||||
case width > 1013:
|
||||
if (len != 4) {
|
||||
set(this, 'columns', [25, 25, 25, 25]);
|
||||
}
|
||||
break;
|
||||
case width > 744:
|
||||
if (len != 3) {
|
||||
set(this, 'columns', [33, 33, 34]);
|
||||
}
|
||||
break;
|
||||
case width > 487:
|
||||
if (len != 2) {
|
||||
set(this, 'columns', [50, 50]);
|
||||
}
|
||||
break;
|
||||
case width < 488:
|
||||
if (len != 1) {
|
||||
set(this, 'columns', [100]);
|
||||
}
|
||||
}
|
||||
if (len !== get(this, 'columns.length')) {
|
||||
this._cellLayout = this['cell-layout'] = new PercentageColumns(
|
||||
get(this, 'items.length'),
|
||||
get(this, 'columns'),
|
||||
get(this, 'cellHeight')
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,9 +14,26 @@ export default Controller.extend(WithEventSource, WithSearching, {
|
|||
};
|
||||
this._super(...arguments);
|
||||
},
|
||||
searchable: computed('items.[]', function() {
|
||||
searchable: computed('services.[]', function() {
|
||||
return get(this, 'searchables.service')
|
||||
.add(this.items)
|
||||
.add(this.services)
|
||||
.search(this.terms);
|
||||
}),
|
||||
services: computed('items.[]', function() {
|
||||
return this.items.filter(function(item) {
|
||||
return item.Kind === 'consul';
|
||||
});
|
||||
}),
|
||||
proxies: computed('items.[]', function() {
|
||||
return this.items.filter(function(item) {
|
||||
return item.Kind === 'connect-proxy';
|
||||
});
|
||||
}),
|
||||
withProxies: computed('proxies', function() {
|
||||
const proxies = {};
|
||||
this.proxies.forEach(item => {
|
||||
proxies[item.Name.replace('-proxy', '')] = true;
|
||||
});
|
||||
return proxies;
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export function healthChecks([item], hash) {
|
||||
switch (true) {
|
||||
case item.ChecksCritical !== 0:
|
||||
return 'critical';
|
||||
case item.ChecksWarning !== 0:
|
||||
return 'warning';
|
||||
case item.ChecksPassing !== 0:
|
||||
return 'passing';
|
||||
default:
|
||||
return 'empty';
|
||||
}
|
||||
}
|
||||
|
||||
export default helper(healthChecks);
|
|
@ -13,6 +13,7 @@ export default Model.extend({
|
|||
return [];
|
||||
},
|
||||
}),
|
||||
InstanceCount: attr('number'),
|
||||
Kind: attr('string'),
|
||||
ExternalSources: attr(),
|
||||
Meta: attr(),
|
||||
|
|
|
@ -153,6 +153,7 @@ $sub-right-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" f
|
|||
$support-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2c5.52.006 9.994 4.48 10 10 0 5.523-4.477 10-10 10S2 17.523 2 12zm17.83-2.588a.208.208 0 0 0 .027-.19 8.376 8.376 0 0 0-5.079-5.079.209.209 0 0 0-.278.197v3.213c0 .074.04.142.102.18.68.416 1.251.988 1.667 1.667a.21.21 0 0 0 .179.1h3.213a.208.208 0 0 0 .17-.088zM12 15.333a3.333 3.333 0 1 1 0-6.666 3.333 3.333 0 0 1 0 6.666zM9.412 4.17a.21.21 0 0 0-.19-.027A8.376 8.376 0 0 0 4.14 9.227a.206.206 0 0 0 .026.19.21.21 0 0 0 .172.083h3.213a.21.21 0 0 0 .181-.102c.416-.68.988-1.25 1.667-1.666a.21.21 0 0 0 .1-.179V4.34a.21.21 0 0 0-.088-.17zM4.143 14.778a.207.207 0 0 1 .196-.278h3.213a.21.21 0 0 1 .179.1c.416.68.987 1.25 1.666 1.667a.21.21 0 0 1 .1.178v3.213a.208.208 0 0 1-.278.196 8.376 8.376 0 0 1-5.076-5.076zm10.446 5.054a.208.208 0 0 0 .19.026 8.376 8.376 0 0 0 5.072-5.077.208.208 0 0 0-.192-.277h-3.214a.21.21 0 0 0-.178.1A5.042 5.042 0 0 1 14.6 16.27a.209.209 0 0 0-.1.178v3.214c0 .067.033.13.088.17z" fill="%23000"/></svg>');
|
||||
$swap-horizontal-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z" fill="%23000"/></svg>');
|
||||
$swap-vertical-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M16 17.01V10h-2v7.01h-3L15 21l4-3.99h-3zM9 3L5 6.99h3V14h2V6.99h3L9 3z" fill="%23000"/></svg>');
|
||||
$tag-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.0180508,1.53059144 C9.95215711,0.826186691 11.2210727,0.822856755 12.1586551,1.52234975 L17.5225102,5.52410888 C18.2146301,6.04047204 18.6243687,6.86710244 18.6243687,7.7470623 L18.6243687,17.2546969 C18.6243687,18.7708859 17.4304613,20 15.957702,20 L5.29103534,20 C3.818276,20 2.62436867,18.7708859 2.62436867,17.2546969 L2.62436867,7.74412605 C2.62436867,6.9274347 2.97732776,6.15640255 3.58187865,5.63682791 L3.71523922,5.52941433 L9.0180508,1.53059144 Z M4.97500563,7.08324321 C4.75460594,7.25178538 4.62436867,7.51991876 4.62436867,7.80513698 L4.62436867,17.1051579 C4.62436867,17.5993656 5.0081246,18 5.48151153,18 L15.7672258,18 C16.2406127,18 16.6243687,17.5993656 16.6243687,17.1051579 L16.6243687,7.80800823 C16.6243687,7.52118194 16.492667,7.2517386 16.2701999,7.08342805 L11.0979111,3.1702619 C10.7965453,2.94225948 10.3886795,2.94334489 10.0884311,3.17294831 L4.97500563,7.08324321 Z M10.4779221,10.732233 C11.8586339,10.732233 12.9779221,9.61294492 12.9779221,8.23223305 C12.9779221,6.85152117 11.8586339,5.73223305 10.4779221,5.73223305 C9.09721019,5.73223305 7.97792206,6.85152117 7.97792206,8.23223305 C7.97792206,9.61294492 9.09721019,10.732233 10.4779221,10.732233 Z" fill="%23000"/></svg>');
|
||||
$terraform-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="%235C4EE5" d="M5.51 3.15l4.886 2.821v5.644L5.509 8.792z"/><path fill="%234040B2" d="M10.931 5.971v5.644l4.888-2.823V3.15z"/><path fill="%235C4EE5" d="M.086 0v5.642l4.887 2.823V2.82zM5.51 15.053l4.886 2.823v-5.644l-4.887-2.82z"/></g></svg>');
|
||||
$trash-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M21 4v2H3V4h6l1-1h4l1 1h6zm-4 15V7h2v12c0 1.1-.9 2-2 2H7c-1.1 0-2-.9-2-2V7h2v12h10zm-8-2h2V7H9v10zm6 0h-2V7h2v10z" fill="%23000"/></svg>');
|
||||
$tune-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z" fill="%23000"/></svg>');
|
||||
|
|
|
@ -1538,6 +1538,16 @@
|
|||
mask-image: $swap-vertical-svg;
|
||||
}
|
||||
|
||||
%with-tag-icon {
|
||||
@extend %with-icon;
|
||||
background-image: $tag-svg;
|
||||
}
|
||||
%with-tag-mask {
|
||||
@extend %with-mask;
|
||||
-webkit-mask-image: $tag-svg;
|
||||
mask-image: $tag-svg;
|
||||
}
|
||||
|
||||
%with-terraform-logo-color-icon {
|
||||
@extend %with-icon;
|
||||
background-image: $terraform-logo-color-svg;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
@import './layout';
|
||||
@import './skin';
|
||||
%composite-row a:hover,
|
||||
%composite-row a:focus,
|
||||
%composite-row a:active {
|
||||
@extend %composite-row-intent;
|
||||
}
|
||||
%composite-row > a > span {
|
||||
@extend %composite-row-header;
|
||||
}
|
||||
%composite-row > a > ul {
|
||||
@extend %composite-row-detail;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
%composite-row a {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
padding-right: 0;
|
||||
border: 1px solid;
|
||||
border-bottom: 0;
|
||||
}
|
||||
%composite-row-intent {
|
||||
border: 1px solid;
|
||||
position: relative;
|
||||
}
|
||||
%composite-row-detail {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
%composite-row-detail * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
%composite-row-detail > li:not(:first-child) {
|
||||
margin-left: 12px;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
%composite-row {
|
||||
list-style-type: none;
|
||||
}
|
||||
%composite-row a {
|
||||
border-top-color: $gray-200;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
%composite-row-intent {
|
||||
border-color: $gray-200;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
%composite-row-header {
|
||||
color: $black;
|
||||
}
|
||||
%composite-row-detail {
|
||||
color: $gray-500;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
@import './consul-service-list/index';
|
||||
|
||||
.consul-service-list > ul {
|
||||
@extend %consul-service-list;
|
||||
}
|
||||
%consul-service-list > li {
|
||||
@extend %consul-service-row;
|
||||
}
|
||||
%consul-service-row {
|
||||
@extend %composite-row;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
@import './layout';
|
||||
@import './skin';
|
|
@ -0,0 +1,21 @@
|
|||
%consul-service-list {
|
||||
// Used for every DOM-Recycle scroll pane
|
||||
// TODO: Refactor to have all this DOM-Recycle styling in one place
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
%consul-service-row > a > span:first-child {
|
||||
margin-right: 4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
%consul-service-row > a > span:nth-child(3) {
|
||||
margin-left: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
%consul-service-row > a > ul {
|
||||
margin-left: 26px;
|
||||
}
|
||||
%consul-service-row .proxy::before {
|
||||
margin-right: 4px;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
%consul-service-row > a > span:nth-child(2) {
|
||||
font-size: 1.125rem;
|
||||
font-weight: $typo-weight-medium;
|
||||
}
|
||||
%consul-service-row > a > span:first-child,
|
||||
%consul-service-row > a > span:nth-child(3) {
|
||||
@extend %as-pseudo;
|
||||
}
|
||||
%consul-service-row .empty {
|
||||
@extend %with-minus-square-fill-color-mask;
|
||||
background-color: #7c8797;
|
||||
}
|
||||
%consul-service-row .kubernetes {
|
||||
@extend %with-kubernetes-logo-color-icon;
|
||||
}
|
||||
%consul-service-row .terraform {
|
||||
@extend %with-terraform-logo-color-icon;
|
||||
}
|
||||
%consul-service-row .nomad {
|
||||
@extend %with-nomad-logo-color-icon;
|
||||
}
|
||||
%consul-service-row .consul {
|
||||
@extend %with-consul-logo-color-icon;
|
||||
}
|
||||
%consul-service-row .aws {
|
||||
@extend %with-logo-aws-color-icon;
|
||||
}
|
||||
%consul-service-row .passing {
|
||||
@extend %with-check-circle-fill-color-mask;
|
||||
background-color: $green-500;
|
||||
}
|
||||
%consul-service-row .warning {
|
||||
@extend %with-alert-triangle-color-mask;
|
||||
background-color: $orange-500;
|
||||
}
|
||||
%consul-service-row .critical {
|
||||
@extend %with-cancel-square-fill-color-mask;
|
||||
background-color: $red-500;
|
||||
}
|
||||
%consul-service-row .proxy::before {
|
||||
@extend %with-swap-horizontal-mask, %as-pseudo;
|
||||
background-color: $gray-500;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
.unhealthy > div,
|
||||
.healthy > div {
|
||||
@extend %card-grid;
|
||||
}
|
||||
.grid-collection {
|
||||
height: 500px;
|
||||
position: relative;
|
||||
}
|
||||
.healthy > div {
|
||||
width: calc(100% + 23px);
|
||||
min-height: 500px;
|
||||
}
|
||||
.unhealthy > div {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.healthy > div > ul > li {
|
||||
padding-right: 23px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
%card-grid > ul,
|
||||
%card-grid > ol {
|
||||
list-style-type: none;
|
||||
display: grid;
|
||||
grid-auto-rows: 12px;
|
||||
}
|
||||
@media #{$--fixed-grid} {
|
||||
%card-grid > ul,
|
||||
%card-grid > ol {
|
||||
grid-gap: 20px 20px;
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
@media #{$--lt-fixed-grid} {
|
||||
%card-grid > ul,
|
||||
%card-grid > ol {
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-gap: 20px 2%;
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
@import './anchors';
|
||||
@import './progress';
|
||||
@import './buttons';
|
||||
@import './composite-row';
|
||||
@import './secret-button';
|
||||
@import './tabs';
|
||||
@import './pill';
|
||||
|
@ -31,6 +32,8 @@
|
|||
@import './tabular-details';
|
||||
@import './tabular-collection';
|
||||
@import './list-collection';
|
||||
@import './grid-collection';
|
||||
@import './consul-service-list';
|
||||
|
||||
/**/
|
||||
|
||||
|
|
|
@ -1,39 +1,4 @@
|
|||
.unhealthy > div,
|
||||
.healthy > div {
|
||||
@extend %card-grid;
|
||||
}
|
||||
.list-collection {
|
||||
height: 500px;
|
||||
position: relative;
|
||||
}
|
||||
.healthy > div {
|
||||
width: calc(100% + 23px);
|
||||
min-height: 500px;
|
||||
}
|
||||
.unhealthy > div {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.healthy > div > ul > li {
|
||||
padding-right: 23px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
%card-grid > ul,
|
||||
%card-grid > ol {
|
||||
list-style-type: none;
|
||||
display: grid;
|
||||
grid-auto-rows: 12px;
|
||||
}
|
||||
@media #{$--fixed-grid} {
|
||||
%card-grid > ul,
|
||||
%card-grid > ol {
|
||||
grid-gap: 20px 20px;
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
@media #{$--lt-fixed-grid} {
|
||||
%card-grid > ul,
|
||||
%card-grid > ol {
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-gap: 20px 2%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
@import '../base/components/pill/index';
|
||||
td strong,
|
||||
%tag-list span {
|
||||
td strong {
|
||||
@extend %pill;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
// the default definition list layout used in edit pages
|
||||
// ideally we'd be more specific with those to say
|
||||
// only add padding to dl's in edit pages
|
||||
%tag-list::before {
|
||||
@extend %with-tag-mask, %as-pseudo;
|
||||
transform: rotate(-45deg);
|
||||
margin-right: 4px;
|
||||
}
|
||||
%tag-list dd {
|
||||
display: inline-flex;
|
||||
padding-left: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
padding-left: 0px;
|
||||
}
|
||||
%tag-list dd:after {
|
||||
content: '';
|
||||
flex: auto;
|
||||
}
|
||||
%tag-list dd > * {
|
||||
margin-right: 3px;
|
||||
margin-bottom: 10px;
|
||||
%tag-list dd > *:not(:last-child)::after {
|
||||
content: ', ';
|
||||
white-space: pre;
|
||||
display: inline;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
%tag-list::before {
|
||||
background-color: $gray-500;
|
||||
}
|
|
@ -45,7 +45,7 @@
|
|||
<h2>Healthy Nodes</h2>
|
||||
<ChangeableSet @dispatcher={{searchableHealthy}}>
|
||||
<BlockSlot @name="set" as |healthy|>
|
||||
<ListCollection @cellHeight={{92}} @items={{healthy}} as |item index|>
|
||||
<GridCollection @cellHeight={{92}} @items={{healthy}} as |item index|>
|
||||
<HealthcheckedResource @data-test-node={{item.Node}} @href={{href-to "dc.nodes.show" item.Node}} @name={{item.Node}} @address={{item.Address}} @checks={{item.Checks}}>
|
||||
<BlockSlot @name="icon">
|
||||
{{#if (eq item.Address leader.Address)}}
|
||||
|
@ -53,7 +53,7 @@
|
|||
{{/if}}
|
||||
</BlockSlot>
|
||||
</HealthcheckedResource>
|
||||
</ListCollection>
|
||||
</GridCollection>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<p>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<AppView @class="service list">
|
||||
<BlockSlot @name="header">
|
||||
<h1>
|
||||
Services <em>{{format-number items.length}} total</em>
|
||||
Services <em>{{format-number services.length}} total</em>
|
||||
</h1>
|
||||
<label for="toolbar-toggle"></label>
|
||||
</BlockSlot>
|
||||
|
@ -14,7 +14,15 @@
|
|||
<BlockSlot @name="content">
|
||||
<ChangeableSet @dispatcher={{searchable}}>
|
||||
<BlockSlot @name="set" as |filtered|>
|
||||
<ConsulServiceList @routeName="dc.services.show" @items={{filtered}} />
|
||||
<ConsulServiceList @routeName="dc.services.show" @items={{filtered}}>
|
||||
<BlockSlot @name="metadata" as |item|>
|
||||
<ul>
|
||||
<li>{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'Instance' without-count=true}}</li>
|
||||
{{#if (get withProxies item.Name)}}<li class="proxy">connected with proxy</li>{{/if}}
|
||||
<li><TagList @items={{item.Tags}}/></li>
|
||||
</ul>
|
||||
</BlockSlot>
|
||||
</ConsulServiceList>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<p>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
"jsonlint": "^1.6.3",
|
||||
"lint-staged": "^9.2.5",
|
||||
"loader.js": "^4.7.0",
|
||||
"ngraph.graph": "^18.0.3",
|
||||
"mnemonist": "^0.30.0",
|
||||
"ngraph.graph": "^18.0.3",
|
||||
"node-sass": "^4.9.3",
|
||||
"pretender": "^3.2.0",
|
||||
"prettier": "^1.10.2",
|
||||
|
|
|
@ -129,15 +129,21 @@ Feature: components / catalog-filter
|
|||
Given 1 datacenter model with the value "dc-1"
|
||||
And 3 service models from yaml
|
||||
---
|
||||
- Tags: ['one', 'two', 'three']
|
||||
- Name: Service-0
|
||||
Kind: consul
|
||||
Tags: ['one', 'two', 'three']
|
||||
ChecksPassing: 0
|
||||
ChecksWarning: 0
|
||||
ChecksCritical: 1
|
||||
- Tags: ['two', 'three']
|
||||
- Name: Service-1
|
||||
Kind: consul
|
||||
Tags: ['two', 'three']
|
||||
ChecksPassing: 0
|
||||
ChecksWarning: 1
|
||||
ChecksCritical: 0
|
||||
- Tags: ['three']
|
||||
- Name: Service-2
|
||||
Kind: consul
|
||||
Tags: ['three']
|
||||
ChecksPassing: 1
|
||||
ChecksWarning: 0
|
||||
ChecksCritical: 0
|
||||
|
@ -162,4 +168,4 @@ Feature: components / catalog-filter
|
|||
---
|
||||
s: 'status:critical'
|
||||
---
|
||||
And I see 1 service model
|
||||
And I see 1 service model
|
|
@ -6,7 +6,15 @@ Feature: dc / error: Recovering from a dc 500 error
|
|||
- dc-1
|
||||
- dc-500
|
||||
---
|
||||
And 3 service models
|
||||
And 3 service models from yaml
|
||||
---
|
||||
- Name: Service-0
|
||||
Kind: consul
|
||||
- Name: Service-1
|
||||
Kind: consul
|
||||
- Name: Service-2
|
||||
Kind: consul
|
||||
---
|
||||
And the url "/v1/internal/ui/services" responds with a 500 status
|
||||
When I visit the services page for yaml
|
||||
---
|
||||
|
|
|
@ -23,7 +23,6 @@ Feature: dc / list-blocking
|
|||
Where:
|
||||
------------------------------------------------
|
||||
| Page | Model | Url |
|
||||
| services | service | services |
|
||||
| nodes | node | nodes |
|
||||
| intentions | intention | intentions |
|
||||
------------------------------------------------
|
||||
|
|
|
@ -13,7 +13,6 @@ Feature: dc / list: List Models
|
|||
Where:
|
||||
-------------------------------------------------
|
||||
| Model | Page | Url |
|
||||
| service | services | /dc-1/services |
|
||||
| node | nodes | /dc-1/nodes |
|
||||
| kv | kvs | /dc-1/kv |
|
||||
# | acl | acls | /dc-1/acls |
|
||||
|
|
|
@ -13,7 +13,22 @@ Feature: dc / nspaces / manage : Managing Namespaces
|
|||
---
|
||||
- dc-1
|
||||
---
|
||||
And 6 service models
|
||||
And 6 service models from yaml
|
||||
---
|
||||
- Name: Service-0
|
||||
Kind: consul
|
||||
- Name: Service-1
|
||||
Kind: consul
|
||||
- Name: Service-2
|
||||
Kind: consul
|
||||
- Name: Service-3
|
||||
Kind: consul
|
||||
- Name: Service-4
|
||||
Kind: consul
|
||||
- Name: Service-5
|
||||
Kind: consul
|
||||
---
|
||||
|
||||
When I visit the services page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
|
|
|
@ -6,7 +6,21 @@ Feature: dc / services / dc-switch : Switching Datacenters
|
|||
- dc-1
|
||||
- dc-2
|
||||
---
|
||||
And 6 service models
|
||||
And 6 service models from yaml
|
||||
---
|
||||
- Name: Service-0
|
||||
Kind: consul
|
||||
- Name: Service-1
|
||||
Kind: consul
|
||||
- Name: Service-2
|
||||
Kind: consul
|
||||
- Name: Service-3
|
||||
Kind: consul
|
||||
- Name: Service-4
|
||||
Kind: consul
|
||||
- Name: Service-5
|
||||
Kind: consul
|
||||
---
|
||||
When I visit the services page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
|
|
|
@ -2,24 +2,24 @@
|
|||
Feature: dc / services / index: List Services
|
||||
Scenario:
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 6 service models from yaml
|
||||
And 4 service models from yaml
|
||||
---
|
||||
- Name: Service 1
|
||||
Kind: consul
|
||||
ExternalSources:
|
||||
- consul
|
||||
- Name: Service 2
|
||||
Kind: consul
|
||||
ExternalSources:
|
||||
- nomad
|
||||
- Name: Service 3
|
||||
Kind: consul
|
||||
ExternalSources:
|
||||
- terraform
|
||||
- Name: Service 4
|
||||
Kind: consul
|
||||
ExternalSources:
|
||||
- kubernetes
|
||||
- Name: Service 5
|
||||
ExternalSources: []
|
||||
- Name: Service 6
|
||||
ExternalSources: ~
|
||||
---
|
||||
When I visit the services page for yaml
|
||||
---
|
||||
|
@ -27,14 +27,12 @@ Feature: dc / services / index: List Services
|
|||
---
|
||||
Then the url should be /dc-1/services
|
||||
And the title should be "Services - Consul"
|
||||
Then I see 6 service models
|
||||
Then I see 4 service models
|
||||
And I see externalSource on the services like yaml
|
||||
---
|
||||
- consul
|
||||
- nomad
|
||||
- terraform
|
||||
- kubernetes
|
||||
- ~
|
||||
- ~
|
||||
---
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / services / list blocking
|
||||
Scenario: Viewing the listing pages for service
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
Given 3 service models from yaml
|
||||
---
|
||||
- Name: Service-0
|
||||
Kind: consul
|
||||
- Name: Service-1
|
||||
Kind: consul
|
||||
- Name: Service-2
|
||||
Kind: consul
|
||||
---
|
||||
And a network latency of 100
|
||||
When I visit the services page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be /dc-1/services
|
||||
And pause until I see 3 service models
|
||||
And an external edit results in 5 service models
|
||||
And pause until I see 3 service models
|
||||
And an external edit results in 1 service model
|
||||
And pause until I see 1 service model
|
||||
And an external edit results in 0 service models
|
||||
And pause until I see 0 service models
|
|
@ -0,0 +1,20 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / services / list
|
||||
Scenario: Listing service
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 3 service models from yaml
|
||||
---
|
||||
- Name: Service-0
|
||||
Kind: consul
|
||||
- Name: Service-1
|
||||
Kind: consul
|
||||
- Name: Service-2
|
||||
Kind: consul
|
||||
---
|
||||
When I visit the services page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be /dc-1/services
|
||||
|
||||
Then I see 3 service models
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -69,7 +69,7 @@ export default {
|
|||
index: create(index(visitable, collection)),
|
||||
dcs: create(dcs(visitable, clickable, attribute, collection)),
|
||||
services: create(
|
||||
services(visitable, clickable, attribute, collection, page, catalogFilter, radiogroup)
|
||||
services(visitable, clickable, text, attribute, collection, page, catalogFilter, radiogroup)
|
||||
),
|
||||
service: create(service(visitable, attribute, collection, text, catalogFilter, tabgroup)),
|
||||
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
export default function(visitable, clickable, attribute, collection, page, filter) {
|
||||
export default function(visitable, clickable, text, attribute, collection, page, filter) {
|
||||
const service = {
|
||||
name: text('a span:nth-child(2)'),
|
||||
service: clickable('a'),
|
||||
externalSource: attribute('data-test-external-source', '[data-test-external-source]'),
|
||||
};
|
||||
return {
|
||||
visit: visitable('/:dc/services'),
|
||||
services: collection('[data-test-service]', {
|
||||
name: attribute('data-test-service'),
|
||||
service: clickable('a'),
|
||||
externalSource: attribute('data-test-external-source', 'a span'),
|
||||
}),
|
||||
services: collection('.consul-service-list > ul > li:not(:first-child)', service),
|
||||
dcs: collection('[data-test-datacenter-picker]', {
|
||||
name: clickable('a'),
|
||||
}),
|
||||
|
|
|
@ -1211,9 +1211,9 @@
|
|||
js-yaml "^3.13.1"
|
||||
|
||||
"@hashicorp/consul-api-double@^2.6.2":
|
||||
version "2.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.12.0.tgz#725078f770bbd0ef75a5f2498968c5c8891f90a2"
|
||||
integrity sha512-8OcgesUjWQ8AjaXzbz3tGJQn1kM0sN6pLidGM7isNPUyYmIjIEXQzaeUQYzsfv0N2Ko9ZuOXYUsaBl8IK1KGow==
|
||||
version "2.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.14.0.tgz#ecef725fc22490011a671bc0a285a16013ca5e53"
|
||||
integrity sha512-1rGMg/XSHR2ROr8a7OVEwOUy8UWuYdNUMijMxCuFHR201vDAGK9EDmkJCPF2PfYsDrcsiyb/0dxIL6Mba9p32Q==
|
||||
|
||||
"@hashicorp/ember-cli-api-double@^3.0.2":
|
||||
version "3.0.2"
|
||||
|
|
Loading…
Reference in New Issue