UI: Service Instances (#5326)
This gives more prominence to 'Service Instances' as opposed to 'Services'. It also begins to surface Connect related 'nouns' such as 'Proxies' and 'Upstreams' and begins to interconnect them giving more visibility to operators. Various smaller changes: 1. Move healthcheck-status component to healthcheck-output 2. Create a new healthcheck-status component for showing the number of checks plus its icon 3. Create a new healthcheck-info component to group multiple statuses plus a different view if there are no checks 4. Componentize tag-list
This commit is contained in:
parent
eabb308ac2
commit
e0326c3b0a
|
@ -0,0 +1,20 @@
|
|||
import Adapter from './application';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/proxy';
|
||||
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
|
||||
export default Adapter.extend({
|
||||
urlForQuery: function(query, modelName) {
|
||||
if (typeof query.id === 'undefined') {
|
||||
throw new Error('You must specify an id');
|
||||
}
|
||||
// https://www.consul.io/api/catalog.html#list-nodes-for-connect-capable-service
|
||||
return this.appendURL('catalog/connect', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
handleResponse: function(status, headers, payload, requestData) {
|
||||
let response = payload;
|
||||
if (status === HTTP_OK) {
|
||||
const url = this.parseURL(requestData.url);
|
||||
response = this.handleBatchResponse(url, response, PRIMARY_KEY, SLUG_KEY);
|
||||
}
|
||||
return this._super(status, headers, response, requestData);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
import Component from '@ember/component';
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import Component from '@ember/component';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
// TODO: Could potentially do this on attr change
|
||||
actions: {
|
||||
sortChecksByImportance: function(a, b) {
|
||||
const statusA = get(a, 'Status');
|
||||
const statusB = get(b, 'Status');
|
||||
switch (statusA) {
|
||||
case 'passing':
|
||||
// a = passing
|
||||
// unless b is also passing then a is less important
|
||||
return statusB === 'passing' ? 0 : 1;
|
||||
case 'critical':
|
||||
// a = critical
|
||||
// unless b is also critical then a is more important
|
||||
return statusB === 'critical' ? 0 : -1;
|
||||
case 'warning':
|
||||
// a = warning
|
||||
switch (statusB) {
|
||||
// b is passing so a is more important
|
||||
case 'passing':
|
||||
return -1;
|
||||
// b is critical so a is less important
|
||||
case 'critical':
|
||||
return 1;
|
||||
// a and b are both warning, therefore equal
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['healthcheck-output'],
|
||||
});
|
|
@ -1,5 +1,12 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
import { get, computed } from '@ember/object';
|
||||
export default Component.extend({
|
||||
classNames: ['healthcheck-status'],
|
||||
tagName: '',
|
||||
count: computed('value', function() {
|
||||
const value = get(this, 'value');
|
||||
if (Array.isArray(value)) {
|
||||
return value.length;
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -3,4 +3,5 @@ import Component from '@ember/component';
|
|||
export default Component.extend({
|
||||
name: 'tab',
|
||||
tagName: 'nav',
|
||||
classNames: ['tab-nav'],
|
||||
});
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'dl',
|
||||
classNames: ['tag-list'],
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { set } from '@ember/object';
|
||||
|
||||
export default Controller.extend({
|
||||
setProperties: function() {
|
||||
this._super(...arguments);
|
||||
// This method is called immediately after `Route::setupController`, and done here rather than there
|
||||
// as this is a variable used purely for view level things, if the view was different we might not
|
||||
// need this variable
|
||||
set(this, 'selectedTab', 'service-checks');
|
||||
},
|
||||
actions: {
|
||||
change: function(e) {
|
||||
set(this, 'selectedTab', e.target.value);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,38 +1,39 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get, computed } from '@ember/object';
|
||||
import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy';
|
||||
import hasStatus from 'consul-ui/utils/hasStatus';
|
||||
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
|
||||
import { get, set, computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithSearching from 'consul-ui/mixins/with-searching';
|
||||
export default Controller.extend(WithSearching, WithHealthFiltering, {
|
||||
export default Controller.extend(WithSearching, {
|
||||
dom: service('dom'),
|
||||
init: function() {
|
||||
this.searchParams = {
|
||||
healthyServiceNode: 's',
|
||||
unhealthyServiceNode: 's',
|
||||
serviceInstance: 's',
|
||||
};
|
||||
this._super(...arguments);
|
||||
},
|
||||
searchableHealthy: computed('healthy', function() {
|
||||
return get(this, 'searchables.healthyServiceNode')
|
||||
.add(get(this, 'healthy'))
|
||||
.search(get(this, this.searchParams.healthyServiceNode));
|
||||
setProperties: function() {
|
||||
this._super(...arguments);
|
||||
// This method is called immediately after `Route::setupController`, and done here rather than there
|
||||
// as this is a variable used purely for view level things, if the view was different we might not
|
||||
// need this variable
|
||||
set(this, 'selectedTab', 'instances');
|
||||
},
|
||||
searchable: computed('items', function() {
|
||||
return get(this, 'searchables.serviceInstance')
|
||||
.add(get(this, 'items'))
|
||||
.search(get(this, this.searchParams.serviceInstance));
|
||||
}),
|
||||
searchableUnhealthy: computed('unhealthy', function() {
|
||||
return get(this, 'searchables.unhealthyServiceNode')
|
||||
.add(get(this, 'unhealthy'))
|
||||
.search(get(this, this.searchParams.unhealthyServiceNode));
|
||||
}),
|
||||
unhealthy: computed('filtered', function() {
|
||||
return get(this, 'filtered').filter(function(item) {
|
||||
return sumOfUnhealthy(item.Checks) > 0;
|
||||
});
|
||||
}),
|
||||
healthy: computed('filtered', function() {
|
||||
return get(this, 'filtered').filter(function(item) {
|
||||
return sumOfUnhealthy(item.Checks) === 0;
|
||||
});
|
||||
}),
|
||||
filter: function(item, { s = '', status = '' }) {
|
||||
return hasStatus(get(item, 'Checks'), status);
|
||||
actions: {
|
||||
change: function(e) {
|
||||
set(this, 'selectedTab', e.target.value);
|
||||
// Ensure tabular-collections sizing is recalculated
|
||||
// now it is visible in the DOM
|
||||
get(this, 'dom')
|
||||
.components('.tab-section input[type="radio"]:checked + div table')
|
||||
.forEach(function(item) {
|
||||
if (typeof item.didAppear === 'function') {
|
||||
item.didAppear();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22,8 +22,7 @@ export function initialize(application) {
|
|||
kv: kv(filterable),
|
||||
healthyNode: node(filterable),
|
||||
unhealthyNode: node(filterable),
|
||||
healthyServiceNode: serviceNode(filterable),
|
||||
unhealthyServiceNode: serviceNode(filterable),
|
||||
serviceInstance: serviceNode(filterable),
|
||||
nodeservice: nodeService(filterable),
|
||||
service: service(filterable),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'ID';
|
||||
export default Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
ServiceName: attr('string'),
|
||||
ServiceID: attr('string'),
|
||||
ServiceProxyDestination: attr('string'),
|
||||
});
|
|
@ -18,6 +18,9 @@ export const routes = {
|
|||
show: {
|
||||
_options: { path: '/:name' },
|
||||
},
|
||||
instance: {
|
||||
_options: { path: '/:name/:id' },
|
||||
},
|
||||
},
|
||||
// Nodes represent a consul node
|
||||
nodes: {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
repo: service('repository/service'),
|
||||
proxyRepo: service('repository/proxy'),
|
||||
model: function(params) {
|
||||
const repo = get(this, 'repo');
|
||||
const proxyRepo = get(this, 'proxyRepo');
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
return hash({
|
||||
item: repo.findInstanceBySlug(params.id, params.name, dc),
|
||||
}).then(function(model) {
|
||||
return hash({
|
||||
proxy:
|
||||
get(service, 'Kind') !== 'connect-proxy'
|
||||
? proxyRepo.findInstanceBySlug(params.id, params.name, dc)
|
||||
: null,
|
||||
...model,
|
||||
});
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -19,6 +19,7 @@ export default Route.extend({
|
|||
return {
|
||||
...model,
|
||||
...{
|
||||
// Nodes happen to be the ServiceInstances here
|
||||
items: model.item.Nodes,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import Serializer from './application';
|
||||
import { PRIMARY_KEY } from 'consul-ui/models/proxy';
|
||||
|
||||
export default Serializer.extend({
|
||||
primaryKey: PRIMARY_KEY,
|
||||
});
|
|
@ -70,7 +70,9 @@ export default Service.extend({
|
|||
// with traditional/standard web components you wouldn't actually need this
|
||||
// method as you could just get to their methods from the dom element
|
||||
component: function(selector, context) {
|
||||
// TODO: support passing a dom element, when we need to do that
|
||||
if (typeof selector !== 'string') {
|
||||
return $_(selector);
|
||||
}
|
||||
return $_(this.element(selector, context));
|
||||
},
|
||||
components: function(selector, context) {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
import { PRIMARY_KEY } from 'consul-ui/models/proxy';
|
||||
import { get } from '@ember/object';
|
||||
const modelName = 'proxy';
|
||||
export default RepositoryService.extend({
|
||||
getModelName: function() {
|
||||
return modelName;
|
||||
},
|
||||
getPrimaryKey: function() {
|
||||
return PRIMARY_KEY;
|
||||
},
|
||||
findAllBySlug: function(slug, dc, configuration = {}) {
|
||||
const query = {
|
||||
id: slug,
|
||||
dc: dc,
|
||||
};
|
||||
if (typeof configuration.cursor !== 'undefined') {
|
||||
query.index = configuration.cursor;
|
||||
}
|
||||
return this.get('store').query(this.getModelName(), query);
|
||||
},
|
||||
findInstanceBySlug: function(id, slug, dc, configuration) {
|
||||
return this.findAllBySlug(slug, dc, configuration).then(function(items) {
|
||||
if (get(items, 'length') > 0) {
|
||||
const instance = items.findBy('ServiceProxyDestination', id);
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -7,16 +7,35 @@ export default RepositoryService.extend({
|
|||
},
|
||||
findBySlug: function(slug, dc) {
|
||||
return this._super(...arguments).then(function(item) {
|
||||
const nodes = get(item, 'Nodes');
|
||||
const service = get(nodes, 'firstObject');
|
||||
const tags = nodes
|
||||
.reduce(function(prev, item) {
|
||||
return prev.concat(get(item, 'Service.Tags') || []);
|
||||
}, [])
|
||||
.uniq();
|
||||
set(service, 'Tags', tags);
|
||||
set(service, 'Nodes', nodes);
|
||||
return service;
|
||||
const nodes = get(item, 'Nodes');
|
||||
const service = get(nodes, 'firstObject');
|
||||
const tags = nodes
|
||||
.reduce(function(prev, item) {
|
||||
return prev.concat(get(item, 'Service.Tags') || []);
|
||||
}, [])
|
||||
.uniq();
|
||||
set(service, 'Tags', tags);
|
||||
set(service, 'Nodes', nodes);
|
||||
return service;
|
||||
});
|
||||
},
|
||||
findInstanceBySlug: function(id, slug, dc, configuration) {
|
||||
return this.findBySlug(slug, dc, configuration).then(function(item) {
|
||||
const i = item.Nodes.findIndex(function(item) {
|
||||
return item.Service.ID === id;
|
||||
});
|
||||
if (i !== -1) {
|
||||
const service = item.Nodes[i].Service;
|
||||
service.Node = item.Nodes[i].Node;
|
||||
service.ServiceChecks = item.Nodes[i].Checks.filter(function(item) {
|
||||
return item.ServiceID != '';
|
||||
});
|
||||
service.NodeChecks = item.Nodes[i].Checks.filter(function(item) {
|
||||
return item.ServiceID == '';
|
||||
});
|
||||
return service;
|
||||
}
|
||||
// TODO: probably need to throw a 404 here?
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,6 +10,15 @@
|
|||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
%app-view header dl {
|
||||
float: left;
|
||||
margin-top: 25px;
|
||||
margin-right: 50px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
%app-view header dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
/* units */
|
||||
%app-view {
|
||||
margin-top: 50px;
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
%app-view h2,
|
||||
%app-view header > div:last-of-type {
|
||||
border-bottom: $decor-border-100;
|
||||
}
|
||||
%app-view header > div:last-of-type,
|
||||
%app-view h2 {
|
||||
border-color: $keyline-light;
|
||||
border-bottom: $decor-border-200;
|
||||
}
|
||||
@media #{$--horizontal-selects} {
|
||||
%app-view header h1 {
|
||||
border-bottom: $decor-border-200;
|
||||
}
|
||||
}
|
||||
@media #{$--lt-horizontal-selects} {
|
||||
%app-view header > div > div:last-child {
|
||||
border-bottom: $decor-border-200;
|
||||
}
|
||||
}
|
||||
%app-view header > div > div:last-child,
|
||||
%app-view header h1,
|
||||
%app-view h2 {
|
||||
border-color: $gray-200;
|
||||
}
|
||||
// We know that any sibling navs might have a top border
|
||||
// by default. As its squashed up to a h1, in this
|
||||
// case hide its border to avoid double border
|
||||
@media #{$--horizontal-selects} {
|
||||
%app-view header h1 ~ nav {
|
||||
border-top: 0 !important;
|
||||
}
|
||||
}
|
||||
%app-content div > dl > dd {
|
||||
color: $gray-400;
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
%breadcrumbs a {
|
||||
%breadcrumbs li > * {
|
||||
@extend %with-chevron;
|
||||
}
|
||||
%breadcrumbs li > strong::before {
|
||||
color: $gray-300;
|
||||
}
|
||||
%breadcrumbs li > a::before {
|
||||
color: rgba($color-action, 0.5);
|
||||
}
|
||||
%breadcrumbs ol {
|
||||
list-style-type: none;
|
||||
}
|
||||
%breadcrumbs a {
|
||||
color: $color-action;
|
||||
}
|
||||
%breadcrumbs strong {
|
||||
color: $gray-400;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ form table,
|
|||
%app-content form dl {
|
||||
@extend %form-row;
|
||||
}
|
||||
%app-content [role='radiogroup'] {
|
||||
%app-content form:not(.filter-bar) [role='radiogroup'] {
|
||||
@extend %radio-group;
|
||||
}
|
||||
%radio-group label {
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
@import './healthcheck-info/index';
|
||||
@import './icons/index';
|
||||
tr dl {
|
||||
@extend %healthcheck-info;
|
||||
}
|
||||
td span.zero {
|
||||
@extend %with-no-healthchecks;
|
||||
// TODO: Why isn't this is layout?
|
||||
display: block;
|
||||
text-indent: 20px;
|
||||
color: $gray-400;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
%healthcheck-info {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
float: left;
|
||||
}
|
||||
%healthcheck-info > * {
|
||||
display: block;
|
||||
}
|
||||
%healthcheck-info dt.zero {
|
||||
display: none;
|
||||
}
|
||||
%healthcheck-info dd.zero {
|
||||
visibility: hidden;
|
||||
}
|
||||
%healthcheck-info dt {
|
||||
text-indent: -9000px;
|
||||
}
|
||||
%healthcheck-info dt.warning {
|
||||
overflow: visible;
|
||||
}
|
||||
%healthcheck-info dt.warning::before {
|
||||
top: 7px;
|
||||
}
|
||||
%healthcheck-info dt.warning::after {
|
||||
left: -2px;
|
||||
top: -1px;
|
||||
}
|
||||
%healthcheck-info dd {
|
||||
box-sizing: content-box;
|
||||
margin-left: 22px;
|
||||
padding-right: 10px;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
%healthcheck-info dt.passing {
|
||||
@extend %with-passing;
|
||||
}
|
||||
%healthcheck-info dt.warning {
|
||||
@extend %with-warning;
|
||||
}
|
||||
%healthcheck-info dt.critical {
|
||||
@extend %with-critical;
|
||||
}
|
||||
%healthcheck-info dt.passing,
|
||||
%healthcheck-info dt.passing + dd {
|
||||
color: $color-success;
|
||||
}
|
||||
%healthcheck-info dt.warning,
|
||||
%healthcheck-info dt.warning + dd {
|
||||
color: $color-alert;
|
||||
}
|
||||
%healthcheck-info dt.critical,
|
||||
%healthcheck-info dt.critical + dd {
|
||||
color: $color-failure;
|
||||
}
|
|
@ -1,32 +1,32 @@
|
|||
@import './healthcheck-status/index';
|
||||
@import './healthcheck-output/index';
|
||||
@import './icons/index';
|
||||
.healthcheck-status {
|
||||
@extend %healthcheck-status;
|
||||
.healthcheck-output {
|
||||
@extend %healthcheck-output;
|
||||
}
|
||||
%healthcheck-status.passing {
|
||||
%healthcheck-output.passing {
|
||||
@extend %with-passing;
|
||||
}
|
||||
%healthcheck-status.warning {
|
||||
%healthcheck-output.warning {
|
||||
@extend %with-warning;
|
||||
}
|
||||
%healthcheck-status.critical {
|
||||
%healthcheck-output.critical {
|
||||
@extend %with-critical;
|
||||
}
|
||||
@media #{$--lt-spacious-healthcheck-status} {
|
||||
.healthcheck-status button.copy-btn {
|
||||
@media #{$--lt-spacious-healthcheck-output} {
|
||||
.healthcheck-output button.copy-btn {
|
||||
margin-top: -11px;
|
||||
margin-right: -18px;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
visibility: hidden;
|
||||
}
|
||||
%healthcheck-status {
|
||||
%healthcheck-output {
|
||||
padding-left: 30px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 15px;
|
||||
padding-right: 13px;
|
||||
}
|
||||
%healthcheck-status::before {
|
||||
%healthcheck-output::before {
|
||||
width: 15px !important;
|
||||
height: 15px !important;
|
||||
left: 9px;
|
|
@ -0,0 +1,2 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
|
@ -1,4 +1,4 @@
|
|||
%healthcheck-status::before {
|
||||
%healthcheck-output::before {
|
||||
background-size: 55%;
|
||||
width: 25px !important;
|
||||
height: 25px !important;
|
||||
|
@ -6,25 +6,25 @@
|
|||
top: 20px !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
%healthcheck-status.warning::before {
|
||||
%healthcheck-output.warning::before {
|
||||
background-size: 100%;
|
||||
}
|
||||
%healthcheck-status {
|
||||
%healthcheck-output {
|
||||
padding: 20px 24px;
|
||||
padding-bottom: 26px;
|
||||
padding-left: 57px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
%healthcheck-status pre {
|
||||
%healthcheck-output pre {
|
||||
padding: 12px;
|
||||
}
|
||||
%healthcheck-status .with-feedback {
|
||||
%healthcheck-output .with-feedback {
|
||||
float: right;
|
||||
}
|
||||
%healthcheck-status dt {
|
||||
%healthcheck-output dt {
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
%healthcheck-status dd:first-of-type {
|
||||
%healthcheck-output dd:first-of-type {
|
||||
margin-bottom: 0.6em;
|
||||
}
|
|
@ -1,35 +1,35 @@
|
|||
%healthcheck-status {
|
||||
%healthcheck-output {
|
||||
border-width: 1px;
|
||||
}
|
||||
%healthcheck-status,
|
||||
%healthcheck-status pre {
|
||||
%healthcheck-output,
|
||||
%healthcheck-output pre {
|
||||
border-radius: $decor-radius-100;
|
||||
}
|
||||
%healthcheck-status dd:first-of-type {
|
||||
%healthcheck-output dd:first-of-type {
|
||||
color: $gray-400;
|
||||
}
|
||||
%healthcheck-status pre {
|
||||
%healthcheck-output pre {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
}
|
||||
%healthcheck-status.passing {
|
||||
%healthcheck-output.passing {
|
||||
/* TODO: this should be a frame-gray */
|
||||
// @extend %frame-green-500;
|
||||
color: $gray-900;
|
||||
border-color: $gray-200;
|
||||
border-style: solid;
|
||||
}
|
||||
%healthcheck-status.warning {
|
||||
%healthcheck-output.warning {
|
||||
@extend %frame-yellow-500;
|
||||
color: $gray-900;
|
||||
}
|
||||
%healthcheck-status.critical {
|
||||
%healthcheck-output.critical {
|
||||
@extend %frame-red-500;
|
||||
color: $gray-900;
|
||||
}
|
||||
%healthcheck-status.passing::before {
|
||||
%healthcheck-output.passing::before {
|
||||
background-color: $color-success !important;
|
||||
}
|
||||
%healthcheck-status.critical::before {
|
||||
%healthcheck-output.critical::before {
|
||||
background-color: $color-danger !important;
|
||||
}
|
|
@ -93,12 +93,11 @@
|
|||
}
|
||||
%with-chevron::before {
|
||||
@extend %pseudo-icon;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="6" height="9" xmlns="http://www.w3.org/2000/svg"><path fill="%230068FF" d="M5.771 9H3.527L.334 4.834 3.527.674h2.244l-3.193 4.16z" opacity=".33"/></svg>');
|
||||
content: '❮';
|
||||
width: 6px;
|
||||
height: 9px;
|
||||
background-color: transparent;
|
||||
left: 0;
|
||||
margin-top: -4px;
|
||||
background-color: $color-transparent;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
%with-folder::before {
|
||||
@extend %pseudo-icon;
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
@import './app-view';
|
||||
@import './product';
|
||||
|
||||
@import './healthcheck-status';
|
||||
@import './tag-list';
|
||||
@import './healthcheck-output';
|
||||
@import './healthcheck-info';
|
||||
@import './healthchecked-resource';
|
||||
@import './freetext-filter';
|
||||
@import './filter-bar';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import './pill/index';
|
||||
td strong {
|
||||
td strong,
|
||||
%tag-list span {
|
||||
@extend %pill;
|
||||
}
|
||||
|
|
|
@ -1,41 +1,30 @@
|
|||
@import './icons/index';
|
||||
@import './table/index';
|
||||
|
||||
html.template-service.template-list td:first-child a span,
|
||||
html.template-node.template-show #services td:first-child a span,
|
||||
html.template-service.template-show #instances td:first-child a span {
|
||||
@extend %with-external-source-icon;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
/* This nudges the th in for the external source icons */
|
||||
html.template-node.template-show #services th:first-child,
|
||||
html.template-service.template-show #instances th:first-child,
|
||||
html.template-service.template-list main th:first-child {
|
||||
text-indent: 28px;
|
||||
}
|
||||
|
||||
td.folder {
|
||||
@extend %with-folder;
|
||||
}
|
||||
td dt.passing {
|
||||
@extend %with-passing;
|
||||
}
|
||||
td dt.warning {
|
||||
@extend %with-warning;
|
||||
}
|
||||
td dt.critical {
|
||||
@extend %with-critical;
|
||||
}
|
||||
td span.zero {
|
||||
@extend %with-no-healthchecks;
|
||||
display: block;
|
||||
text-indent: 20px;
|
||||
color: $gray-400;
|
||||
}
|
||||
table:not(.sessions) tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
table:not(.sessions) td:first-child {
|
||||
padding: 0;
|
||||
}
|
||||
td dt.passing,
|
||||
td dt.passing + dd {
|
||||
color: $color-success;
|
||||
}
|
||||
td dt.warning,
|
||||
td dt.warning + dd {
|
||||
color: $color-alert;
|
||||
}
|
||||
td dt.critical,
|
||||
td dt.critical + dd {
|
||||
color: $color-failure;
|
||||
}
|
||||
/* Header Tooltips/Icon*/
|
||||
th {
|
||||
overflow: visible;
|
||||
|
|
|
@ -31,7 +31,7 @@ table th {
|
|||
padding-bottom: 0.6em;
|
||||
}
|
||||
table td,
|
||||
table td a {
|
||||
table td:first-child a {
|
||||
padding: 0.9em 0;
|
||||
}
|
||||
table th,
|
||||
|
@ -50,44 +50,6 @@ td:not(.actions) a {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
// TODO: this isn't specific to table
|
||||
// these are the node health 3 column display
|
||||
tr > * dl {
|
||||
float: left;
|
||||
}
|
||||
td dl {
|
||||
height: 100%;
|
||||
}
|
||||
td dl {
|
||||
display: flex;
|
||||
}
|
||||
td dl > * {
|
||||
display: block;
|
||||
}
|
||||
td dt.zero {
|
||||
display: none;
|
||||
}
|
||||
td dd.zero {
|
||||
visibility: hidden;
|
||||
}
|
||||
td dt {
|
||||
text-indent: -9000px;
|
||||
}
|
||||
td dt.warning {
|
||||
overflow: visible;
|
||||
}
|
||||
td dt.warning::before {
|
||||
top: 7px;
|
||||
}
|
||||
td dt.warning::after {
|
||||
left: -2px;
|
||||
top: -1px;
|
||||
}
|
||||
td dd {
|
||||
box-sizing: content-box;
|
||||
margin-left: 22px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
/* hide actions on narrow screens, you can always click in do everything from there */
|
||||
@media #{$--lt-wide-table} {
|
||||
tr > .actions {
|
||||
|
@ -96,6 +58,8 @@ td dd {
|
|||
}
|
||||
/* ideally these would be in route css files, but left here as they */
|
||||
/* accomplish the same thing (hide non-essential columns for tables) */
|
||||
/* TODO: Move these to component/table.scss for the moment */
|
||||
/* Also mixed with things in component/tabular-collection.scss move those also */
|
||||
@media #{$--lt-medium-table} {
|
||||
/* Policy > Datacenters */
|
||||
html.template-policy.template-list tr > :nth-child(2) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import './tabs/index';
|
||||
main header nav:last-of-type:not(:first-of-type) {
|
||||
.tab-nav {
|
||||
@extend %tab-nav;
|
||||
}
|
||||
.tab-section {
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
/* this keeps in-tab-section toolbars flush to the top, see Node Detail > Services */
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
%tab-nav {
|
||||
clear: both;
|
||||
}
|
||||
@media #{$--horizontal-tabs} {
|
||||
%tab-nav ul {
|
||||
display: flex;
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
%tab-nav {
|
||||
/* %frame-gray-something */
|
||||
border-bottom: $decor-border-100;
|
||||
border-top: $decor-border-200;
|
||||
}
|
||||
%tab-nav {
|
||||
/* %frame-gray-something */
|
||||
border-color: $gray-200;
|
||||
}
|
||||
%tab-nav label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -35,17 +35,16 @@ table.dom-recycling {
|
|||
/* using: */
|
||||
/* calc(<100% divided by number of non-fixed width cells> - <sum of widths of fixed cells divided by number of non-fixed width cells>) */
|
||||
|
||||
html.template-service.template-list td:first-child a span,
|
||||
html.template-node.template-show #services td:first-child a span {
|
||||
@extend %with-external-source-icon;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
/*TODO: trs only live in tables, get rid of table */
|
||||
html.template-service.template-list main table tr {
|
||||
@extend %services-row;
|
||||
}
|
||||
html.template-service.template-show #instances table tr {
|
||||
@extend %instances-row;
|
||||
}
|
||||
html.template-instance.template-show #upstreams table tr {
|
||||
@extend %upstreams-row;
|
||||
}
|
||||
html.template-intention.template-list main table tr {
|
||||
@extend %intentions-row;
|
||||
}
|
||||
|
@ -146,6 +145,12 @@ html.template-node.template-show main table.sessions tr {
|
|||
html.template-token.template-list main table tr td.me ~ td:nth-of-type(5) {
|
||||
display: none;
|
||||
}
|
||||
html.template-service.template-show #instances tr > :nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
%instances-row > * {
|
||||
width: calc(100% / 4);
|
||||
}
|
||||
}
|
||||
|
||||
%kvs-row > *:first-child {
|
||||
|
@ -155,7 +160,7 @@ html.template-node.template-show main table.sessions tr {
|
|||
@extend %table-actions;
|
||||
}
|
||||
%node-services-row > * {
|
||||
width: 33%;
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
%policies-row > * {
|
||||
width: calc(33% - 20px);
|
||||
|
@ -172,3 +177,9 @@ html.template-node.template-show main table.sessions tr {
|
|||
%services-row > * {
|
||||
width: auto;
|
||||
}
|
||||
%instances-row > * {
|
||||
width: calc(100% / 5);
|
||||
}
|
||||
%upstreams-row > * {
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
@import './tag-list/index';
|
||||
.tag-list,
|
||||
td.tags {
|
||||
@extend %tag-list;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
|
@ -0,0 +1,10 @@
|
|||
%tag-list dt {
|
||||
display: none;
|
||||
}
|
||||
// TODO: Currently this is here to overwrite
|
||||
// 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 dd {
|
||||
padding-left: 0;
|
||||
}
|
|
@ -36,10 +36,10 @@ h1,
|
|||
h2,
|
||||
%header-nav,
|
||||
%healthchecked-resource header span,
|
||||
%healthcheck-status dt,
|
||||
%healthcheck-output dt,
|
||||
%copy-button,
|
||||
%app-content div > dl > dt,
|
||||
td a {
|
||||
td:first-child a {
|
||||
font-weight: $typo-weight-semibold;
|
||||
}
|
||||
%form-element > span,
|
||||
|
@ -51,7 +51,7 @@ caption {
|
|||
font-weight: $typo-weight-semibold !important;
|
||||
}
|
||||
th,
|
||||
%breadcrumbs a,
|
||||
%breadcrumbs li > *,
|
||||
%action-group-action,
|
||||
%tab-nav,
|
||||
%tooltip-bubble {
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
@import '../../../components/pill/index';
|
||||
html.template-service.template-show main dl {
|
||||
display: flex;
|
||||
margin-bottom: 1.4em;
|
||||
}
|
||||
html.template-service.template-show main dt {
|
||||
display: none;
|
||||
}
|
||||
// TODO: Generalize this, also see nodes/index
|
||||
html.template-service.template-list td.tags span,
|
||||
html.template-service.template-show main dd span {
|
||||
@extend %pill;
|
||||
}
|
||||
html.template-node.template-show #services th:first-child,
|
||||
html.template-service.template-list main th:first-child {
|
||||
text-indent: 28px;
|
||||
}
|
|
@ -26,8 +26,8 @@ $--lt-wide-footer: '(max-width: 420px)';
|
|||
$--spacious-page-header: '(min-width: 850px)';
|
||||
$--lt-spacious-page-header: '(max-width: 849px)';
|
||||
|
||||
$--spacious-healthcheck-status: '(min-width: 421px)';
|
||||
$--lt-spacious-healthcheck-status: '(max-width: 420px)';
|
||||
$--spacious-healthcheck-output: '(min-width: 421px)';
|
||||
$--lt-spacious-healthcheck-output: '(max-width: 420px)';
|
||||
|
||||
$--wide-form: '(min-width: 421px)';
|
||||
$--lt-wide-form: '(max-width: 420px)';
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{{#if (and (lt passing 1) (lt warning 1) (lt critical 1) )}}
|
||||
<span title="No Healthchecks" class="zero">0</span>
|
||||
{{else}}
|
||||
<dl>
|
||||
{{healthcheck-status width=passingWidth name='passing' value=passing}}
|
||||
{{healthcheck-status width=warningWidth name='warning' value=warning}}
|
||||
{{healthcheck-status width=criticalWidth name='critical' value=critical}}
|
||||
</dl>
|
||||
{{/if}}
|
|
@ -0,0 +1,5 @@
|
|||
<ul data-test-node-healthchecks>
|
||||
{{#each (sort-by (action 'sortChecksByImportance') items) as |check| }}
|
||||
{{healthcheck-output data-test-node-healthcheck=check.Name tagName='li' name=check.Name class=check.Status status=check.Status notes=check.Notes output=check.Output}}
|
||||
{{/each}}
|
||||
</ul>
|
|
@ -0,0 +1,25 @@
|
|||
{{#feedback-dialog type='inline'}}
|
||||
{{#block-slot 'action' as |success error|}}
|
||||
{{#copy-button success=(action success) error=(action error) clipboardText=output title='copy output to clipboard'}}
|
||||
Copy Output
|
||||
{{/copy-button}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'success' as |transition|}}
|
||||
<p class={{transition}}>
|
||||
Copied IP Address!
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'error' as |transition|}}
|
||||
<p class={{transition}}>
|
||||
Sorry, something went wrong!
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/feedback-dialog}}
|
||||
<dl>
|
||||
<dt>{{name}}</dt>
|
||||
<dd>{{notes}}</dd>
|
||||
<dt>Output</dt>
|
||||
<dd>
|
||||
<pre><code>{{output}}</code></pre>
|
||||
</dd>
|
||||
</dl>
|
|
@ -1,25 +1,3 @@
|
|||
{{#feedback-dialog type='inline'}}
|
||||
{{#block-slot 'action' as |success error|}}
|
||||
{{#copy-button success=(action success) error=(action error) clipboardText=output title='copy output to clipboard'}}
|
||||
Copy Output
|
||||
{{/copy-button}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'success' as |transition|}}
|
||||
<p class={{transition}}>
|
||||
Copied IP Address!
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'error' as |transition|}}
|
||||
<p class={{transition}}>
|
||||
Sorry, something went wrong!
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/feedback-dialog}}
|
||||
<dl>
|
||||
<dt>{{name}}</dt>
|
||||
<dd>{{notes}}</dd>
|
||||
<dt>Output</dt>
|
||||
<dd>
|
||||
<pre><code>{{output}}</code></pre>
|
||||
</dd>
|
||||
</dl>
|
||||
{{!-- we use concat here to avoid ember adding returns between words, which causes a layout issue--}}
|
||||
<dt title="{{capitalize name}}" class="{{name}}{{if (lt count 1) ' zero'}}">{{ concat 'Healthchecks ' (capitalize name) }}</dt>
|
||||
<dd title="{{capitalize name}}" class={{if (lt count 1) 'zero'}} style={{width}}>{{format-number count}}</dd>
|
|
@ -0,0 +1,8 @@
|
|||
{{#if (gt items.length 0)}}
|
||||
<dt class="tags">Tags</dt>
|
||||
<dd data-test-tags class="tags">
|
||||
{{#each items as |item|}}
|
||||
<span>{{item}}</span>
|
||||
{{/each}}
|
||||
</dd>
|
||||
{{/if}}
|
|
@ -1,9 +1,5 @@
|
|||
{{#if (gt item.Checks.length 0) }}
|
||||
<ul data-test-node-healthchecks>
|
||||
{{#each (sort-by (action 'sortChecksByImportance') item.Checks) as |check| }}
|
||||
{{healthcheck-status data-test-node-healthcheck=check.Name tagName='li' name=check.Name class=check.Status status=check.Status notes=check.Notes output=check.Output}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{healthcheck-list items=item.Checks}}
|
||||
{{else}}
|
||||
<p>
|
||||
This node has no health checks.
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<td data-test-service-name="{{item.Service}}">
|
||||
<a href={{href-to 'dc.services.show' item.Service}}>
|
||||
<span data-test-external-source="{{service/external-source item}}" style={{{ concat 'background-image: ' (css-var (concat '--' (service/external-source item) '-color-svg') 'none')}}}></span>
|
||||
{{item.Service}}{{#if (not-eq item.ID item.Service) }}<em data-test-service-id="{{item.ID}}">({{item.ID}})</em>{{/if}}
|
||||
{{item.Service}}{{#if (not-eq item.ID item.Service) }} <em data-test-service-id="{{item.ID}}">({{item.ID}})</em>{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-service-port="{{item.Port}}" class="port">
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
{{#if (gt items.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<form class="filter-bar">
|
||||
{{freetext-filter searchable=searchable value=s placeholder="Search"}}
|
||||
</form>
|
||||
{{/if}}
|
||||
{{#changeable-set dispatcher=searchable}}
|
||||
{{#block-slot 'set' as |filtered|}}
|
||||
{{#tabular-collection
|
||||
data-test-instances
|
||||
items=filtered as |item index|
|
||||
}}
|
||||
{{#block-slot 'header'}}
|
||||
<th>ID</th>
|
||||
<th>Node</th>
|
||||
<th>Address</th>
|
||||
<th>Node Checks</th>
|
||||
<th>Service Checks</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-id="{{item.Service.ID}}">
|
||||
<a href={{href-to 'dc.services.instance' item.Service.Service (or item.Service.ID item.Service.Service )}}>
|
||||
<span data-test-external-source="{{service/external-source item.Service}}" style={{{ concat 'background-image: ' (css-var (concat '--' (service/external-source item.Service) '-color-svg') 'none')}}}></span>
|
||||
{{ or item.Service.ID item.Service.Service }}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-node>
|
||||
<a href={{href-to 'dc.nodes.show' item.Node.Node}}>{{item.Node.Node}}</a>
|
||||
</td>
|
||||
<td data-test-address>
|
||||
{{item.Service.Address}}:{{item.Service.Port}}
|
||||
</td>
|
||||
<td>
|
||||
{{#with (reject-by 'ServiceID' '' item.Checks) as |checks|}}
|
||||
{{healthcheck-info
|
||||
passing=(filter-by 'Status' 'passing' checks) warning=(filter-by 'Status' 'warning' checks) critical=(filter-by 'Status' 'critical' checks)
|
||||
}}
|
||||
{{/with}}
|
||||
</td>
|
||||
<td>
|
||||
{{#with (filter-by 'ServiceID' '' item.Checks) as |checks|}}
|
||||
{{healthcheck-info
|
||||
passing=(filter-by 'Status' 'passing' checks) warning=(filter-by 'Status' 'warning' checks) critical=(filter-by 'Status' 'critical' checks)
|
||||
}}
|
||||
{{/with}}
|
||||
</td>
|
||||
{{/block-slot}}
|
||||
{{/tabular-collection}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no services.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
|
@ -0,0 +1,8 @@
|
|||
{{#if (gt item.NodeChecks.length 0) }}
|
||||
{{healthcheck-list items=item.NodeChecks}}
|
||||
{{else}}
|
||||
<p>
|
||||
This instance has no node health checks.
|
||||
</p>
|
||||
{{/if}}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{{#if (gt item.ServiceChecks.length 0) }}
|
||||
{{healthcheck-list items=item.ServiceChecks}}
|
||||
{{else}}
|
||||
<p>
|
||||
This instance has no service health checks.
|
||||
</p>
|
||||
{{/if}}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{{#if (gt item.Tags.length 0) }}
|
||||
{{tag-list items=item.Tags}}
|
||||
{{else}}
|
||||
<p>
|
||||
There are no tags.
|
||||
</p>
|
||||
{{/if}}
|
|
@ -0,0 +1,27 @@
|
|||
{{#if (gt item.Proxy.Upstreams.length 0) }}
|
||||
{{#tabular-collection
|
||||
data-test-upstreams
|
||||
items=item.Proxy.Upstreams as |item index|
|
||||
}}
|
||||
{{#block-slot 'header'}}
|
||||
<th>Destination Name</th>
|
||||
<th>Destination Type</th>
|
||||
<th>Local Bind Port</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-destination-name>
|
||||
<a>{{item.DestinationName}}</a>
|
||||
</td>
|
||||
<td data-test-destination-type>
|
||||
{{item.DestinationType}}
|
||||
</td>
|
||||
<td data-test-local-bind-port>
|
||||
{{item.LocalBindPort}}
|
||||
</td>
|
||||
{{/block-slot}}
|
||||
{{/tabular-collection}}
|
||||
{{else}}
|
||||
<p>
|
||||
There are no upstreams.
|
||||
</p>
|
||||
{{/if}}
|
|
@ -35,18 +35,10 @@
|
|||
</a>
|
||||
</td>
|
||||
<td style={{totalWidth}}>
|
||||
{{#if (and (lt item.ChecksPassing 1) (lt item.ChecksWarning 1) (lt item.ChecksCritical 1) )}}
|
||||
<span title="No Healthchecks" class="zero">0</span>
|
||||
{{else}}
|
||||
<dl>
|
||||
<dt title="Passing" class="passing{{if (lt item.ChecksPassing 1) ' zero'}}">Healthchecks Passing</dt>
|
||||
<dd title="Passing" class={{if (lt item.ChecksPassing 1) 'zero'}} style={{passingWidth}}>{{format-number item.ChecksPassing}}</dd>
|
||||
<dt title="Warning" class="warning{{if (lt item.ChecksWarning 1) ' zero'}}">Healthchecks Warning</dt>
|
||||
<dd title="Warning" class={{if (lt item.ChecksWarning 1) 'zero'}} style={{warningWidth}}>{{format-number item.ChecksWarning}}</dd>
|
||||
<dt title="Critical" class="critical{{if (lt item.ChecksCritical 1) ' zero'}}">Healthchecks Critical</dt>
|
||||
<dd title="Critical" class={{if (lt item.ChecksCritical 1) 'zero'}} style={{criticalWidth}}>{{format-number item.ChecksCritical}}</dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
{{healthcheck-info
|
||||
passing=item.ChecksPassing warning=item.ChecksWarning critical=item.ChecksCritical
|
||||
passingWidth=passingWidth warningWidth=warningWidth criticalWidth=criticalWidth
|
||||
}}
|
||||
</td>
|
||||
<td class="tags" style={{remainingWidth}}>
|
||||
{{#if (gt item.Tags.length 0)}}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
{{#app-view class="instance show"}}
|
||||
{{#block-slot 'breadcrumbs'}}
|
||||
<ol>
|
||||
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
|
||||
<li><a data-test-back href={{href-to 'dc.services.show'}}>Service ({{item.Service}})</a></li>
|
||||
<li><strong>Instance</strong></li>
|
||||
</ol>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'header'}}
|
||||
<h1>
|
||||
{{ item.ID }}
|
||||
{{#with (service/external-source item) as |externalSource| }}
|
||||
{{#with (css-var (concat '--' externalSource '-color-svg') 'none') as |bg| }}
|
||||
{{#if (not-eq bg 'none') }}
|
||||
<span data-test-external-source="{{externalSource}}" style={{{ concat 'background-image:' bg }}} data-tooltip="Registered via {{externalSource}}">Registered via {{externalSource}}</span>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{/with}}
|
||||
</h1>
|
||||
<dl>
|
||||
<dt>Service Name</dt>
|
||||
<dd><a href="{{href-to 'dc.services.show' item.Service}}">{{item.Service}}</a></dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Node Name</dt>
|
||||
<dd><a href="{{href-to 'dc.nodes.show' item.Node.Node}}">{{item.Node.Node}}</a></dd>
|
||||
</dl>
|
||||
{{#if proxy}}
|
||||
<dl>
|
||||
<dt>Sidecar Proxy</dt>
|
||||
<dd><a href="{{href-to 'dc.services.instance' proxy.ServiceName proxy.ServiceID}}">{{proxy.ServiceID}}</a></dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
{{#if (eq item.Kind 'connect-proxy')}}
|
||||
<dl>
|
||||
<dt>Dest. Service Instance</dt>
|
||||
<dd><a href="{{href-to 'dc.services.instance' item.Proxy.DestinationServiceName item.Proxy.DestinationServiceID}}">{{item.Proxy.DestinationServiceID}}</a></dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Local Service Address</dt>
|
||||
<dd>{{item.Proxy.LocalServiceAddress}}:{{item.Proxy.LocalServicePort}}</dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'content'}}
|
||||
{{tab-nav
|
||||
items=(compact
|
||||
(array
|
||||
'Service Checks'
|
||||
'Node Checks'
|
||||
(if (eq item.Kind 'connect-proxy') 'Upstreams' '')
|
||||
'Tags'
|
||||
)
|
||||
)
|
||||
selected=selectedTab
|
||||
}}
|
||||
{{#each
|
||||
(compact
|
||||
(array
|
||||
(hash id=(slugify 'Service Checks') partial='dc/services/servicechecks')
|
||||
(hash id=(slugify 'Node Checks') partial='dc/services/nodechecks')
|
||||
(if (eq item.Kind 'connect-proxy') (hash id=(slugify 'Upstreams') partial='dc/services/upstreams') '')
|
||||
(hash id=(slugify 'Tags') partial='dc/services/tags')
|
||||
)
|
||||
) as |panel|
|
||||
}}
|
||||
{{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab '') panel.id) onchange=(action "change")}}
|
||||
{{partial panel.partial}}
|
||||
{{/tab-section}}
|
||||
{{/each}}
|
||||
{{/block-slot}}
|
||||
{{/app-view}}
|
|
@ -15,76 +15,29 @@
|
|||
{{/with}}
|
||||
{{/with}}
|
||||
</h1>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'toolbar'}}
|
||||
{{#if (gt items.length 0) }}
|
||||
{{catalog-filter searchable=(array searchableHealthy searchableUnhealthy) filters=healthFilters search=s status=filters.status onchange=(action 'filter')}}
|
||||
{{/if}}
|
||||
<label for="toolbar-toggle"></label>
|
||||
{{tab-nav
|
||||
items=(compact
|
||||
(array
|
||||
'Instances'
|
||||
'Tags'
|
||||
)
|
||||
)
|
||||
selected=selectedTab
|
||||
}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'content'}}
|
||||
{{#if (gt item.Tags.length 0)}}
|
||||
<dl>
|
||||
<dt>Tags</dt>
|
||||
<dd data-test-tags>
|
||||
{{#each item.Tags as |item|}}
|
||||
<span>{{item}}</span>
|
||||
{{/each}}
|
||||
</dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
{{#if (gt unhealthy.length 0) }}
|
||||
<div data-test-unhealthy class="unhealthy">
|
||||
<h2>Unhealthy Nodes</h2>
|
||||
<div>
|
||||
<ul>
|
||||
{{#changeable-set dispatcher=searchableUnhealthy}}
|
||||
{{#block-slot 'set' as |unhealthy|}}
|
||||
{{#each unhealthy as |item|}}
|
||||
{{healthchecked-resource
|
||||
tagName='li'
|
||||
data-test-node=item.Node.Node
|
||||
href=(href-to 'dc.nodes.show' item.Node.Node)
|
||||
name=item.Node.Node
|
||||
service=item.Service.ID
|
||||
address=(concat (default item.Service.Address item.Node.Address) ':' item.Service.Port)
|
||||
checks=item.Checks
|
||||
}}
|
||||
{{/each}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no unhealthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (gt healthy.length 0) }}
|
||||
<div data-test-healthy class="healthy">
|
||||
<h2>Healthy Nodes</h2>
|
||||
{{#changeable-set dispatcher=searchableHealthy}}
|
||||
{{#block-slot 'set' as |healthy|}}
|
||||
{{#list-collection cellHeight=113 items=healthy as |item index|}}
|
||||
{{healthchecked-resource
|
||||
href=(href-to 'dc.nodes.show' item.Node.Node)
|
||||
data-test-node=item.Node.Node
|
||||
name=item.Node.Node
|
||||
service=item.Service.ID
|
||||
address=(concat (default item.Service.Address item.Node.Address) ':' item.Service.Port)
|
||||
checks=item.Checks
|
||||
status=item.Checks.[0].Status
|
||||
}}
|
||||
{{/list-collection}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no healthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#each
|
||||
(compact
|
||||
(array
|
||||
(hash id=(slugify 'Instances') partial='dc/services/instances')
|
||||
(hash id=(slugify 'Tags') partial='dc/services/tags')
|
||||
)
|
||||
) as |panel|
|
||||
}}
|
||||
{{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab '') panel.id) onchange=(action "change")}}
|
||||
{{partial panel.partial}}
|
||||
{{/tab-section}}
|
||||
{{/each}}
|
||||
{{/block-slot}}
|
||||
{{/app-view}}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { get } from '@ember/object';
|
||||
import { get, computed } from '@ember/object';
|
||||
|
||||
/**
|
||||
* Converts a conventional non-pure Ember `computed` function into a pure one
|
||||
|
@ -8,20 +8,18 @@ import { get } from '@ember/object';
|
|||
* @param {function} filter - Optional string filter function to pre-process the names of computed properties
|
||||
* @returns {function} - A pure `computed` function
|
||||
*/
|
||||
|
||||
export default function(computed, filter) {
|
||||
const _success = function(value) {
|
||||
return value;
|
||||
};
|
||||
const purify = function(computed, filter = args => args) {
|
||||
return function() {
|
||||
let args = [...arguments];
|
||||
let success = function(value) {
|
||||
return value;
|
||||
};
|
||||
let success = _success;
|
||||
// pop the user function off the end
|
||||
if (typeof args[args.length - 1] === 'function') {
|
||||
success = args.pop();
|
||||
}
|
||||
if (typeof filter === 'function') {
|
||||
args = filter(args);
|
||||
}
|
||||
args = filter(args);
|
||||
// this is the 'conventional' `computed`
|
||||
const cb = function(name) {
|
||||
return success.apply(
|
||||
|
@ -39,4 +37,6 @@ export default function(computed, filter) {
|
|||
// concat/push the user function back on
|
||||
return computed(...args.concat([cb]));
|
||||
};
|
||||
}
|
||||
};
|
||||
export const subscribe = purify(computed);
|
||||
export default purify;
|
||||
|
|
|
@ -123,31 +123,6 @@ Feature: components / catalog-filter
|
|||
| Model | Page | Url |
|
||||
| service | node | /dc-1/nodes/node-0 |
|
||||
-------------------------------------------------
|
||||
Scenario: Filtering [Model] in [Page]
|
||||
Given 1 datacenter model with the value "dc1"
|
||||
And 2 [Model] models from yaml
|
||||
---
|
||||
- ID: node-0
|
||||
---
|
||||
When I visit the [Page] page for yaml
|
||||
---
|
||||
dc: dc1
|
||||
service: service-0
|
||||
---
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: service-0-with-id
|
||||
---
|
||||
And I see 1 [Model] model
|
||||
Then I see id on the unhealthy like yaml
|
||||
---
|
||||
- service-0-with-id
|
||||
---
|
||||
Where:
|
||||
-------------------------------------------------
|
||||
| Model | Page | Url |
|
||||
| nodes | service | /dc-1/services/service-0 |
|
||||
-------------------------------------------------
|
||||
Scenario:
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 3 service models from yaml
|
||||
|
|
|
@ -52,7 +52,7 @@ Feature: dc / services / show: Show Service
|
|||
Then I see the text "Tag1" in "[data-test-tags] span:nth-child(1)"
|
||||
Then I see the text "Tag2" in "[data-test-tags] span:nth-child(2)"
|
||||
Then I see the text "Tag3" in "[data-test-tags] span:nth-child(3)"
|
||||
Scenario: Given various services the various ports on their nodes are displayed
|
||||
Scenario: Given various services the various nodes on their instances are displayed
|
||||
Given 1 datacenter model with the value "dc1"
|
||||
And 3 node models
|
||||
And 1 service model from yaml
|
||||
|
@ -83,21 +83,9 @@ Feature: dc / services / show: Show Service
|
|||
dc: dc1
|
||||
service: service-0
|
||||
---
|
||||
Then I see address on the healthy like yaml
|
||||
Then I see address on the instances like yaml
|
||||
---
|
||||
- "1.1.1.1:8080"
|
||||
---
|
||||
Then I see address on the unhealthy like yaml
|
||||
---
|
||||
- "2.2.2.2:8000"
|
||||
- "3.3.3.3:8888"
|
||||
---
|
||||
Then I see id on the healthy like yaml
|
||||
---
|
||||
- "passing-service-8080"
|
||||
---
|
||||
Then I see id on the unhealthy like yaml
|
||||
---
|
||||
- "service-8000"
|
||||
- "service-8888"
|
||||
---
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { moduleForComponent, test } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
moduleForComponent('healthcheck-info', 'Integration | Component | healthcheck info', {
|
||||
integration: true,
|
||||
});
|
||||
|
||||
test('it renders', function(assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
|
||||
this.render(hbs`{{healthcheck-info}}`);
|
||||
|
||||
assert.equal(this.$('dl').length, 1);
|
||||
|
||||
// Template block usage:
|
||||
this.render(hbs`
|
||||
{{#healthcheck-info}}
|
||||
{{/healthcheck-info}}
|
||||
`);
|
||||
assert.equal(this.$('dl').length, 1);
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import { moduleForComponent, test } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
moduleForComponent('healthcheck-list', 'Integration | Component | healthcheck list', {
|
||||
integration: true,
|
||||
});
|
||||
|
||||
test('it renders', function(assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
|
||||
this.render(hbs`{{healthcheck-list}}`);
|
||||
|
||||
assert.equal(this.$('ul').length, 1);
|
||||
|
||||
// Template block usage:
|
||||
this.render(hbs`
|
||||
{{#healthcheck-list}}
|
||||
{{/healthcheck-list}}
|
||||
`);
|
||||
|
||||
assert.equal(this.$('ul').length, 1);
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import { moduleForComponent, test } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
moduleForComponent('healthcheck-output', 'Integration | Component | healthcheck output', {
|
||||
integration: true,
|
||||
});
|
||||
|
||||
test('it renders', function(assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
|
||||
this.render(hbs`{{healthcheck-output}}`);
|
||||
|
||||
assert.notEqual(
|
||||
this.$()
|
||||
.text()
|
||||
.trim()
|
||||
.indexOf('Output'),
|
||||
-1
|
||||
);
|
||||
|
||||
// Template block usage:
|
||||
this.render(hbs`
|
||||
{{#healthcheck-output}}{{/healthcheck-output}}
|
||||
`);
|
||||
|
||||
assert.notEqual(
|
||||
this.$()
|
||||
.text()
|
||||
.trim()
|
||||
.indexOf('Output'),
|
||||
-1
|
||||
);
|
||||
});
|
|
@ -10,25 +10,11 @@ test('it renders', function(assert) {
|
|||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
|
||||
this.render(hbs`{{healthcheck-status}}`);
|
||||
|
||||
assert.notEqual(
|
||||
this.$()
|
||||
.text()
|
||||
.trim()
|
||||
.indexOf('Output'),
|
||||
-1
|
||||
);
|
||||
assert.equal(this.$('dt').length, 1);
|
||||
|
||||
// Template block usage:
|
||||
this.render(hbs`
|
||||
{{#healthcheck-status}}{{/healthcheck-status}}
|
||||
`);
|
||||
|
||||
assert.notEqual(
|
||||
this.$()
|
||||
.text()
|
||||
.trim()
|
||||
.indexOf('Output'),
|
||||
-1
|
||||
);
|
||||
assert.equal(this.$('dt').length, 1);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { moduleForComponent, test } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
moduleForComponent('tag-list', 'Integration | Component | tag list', {
|
||||
integration: true,
|
||||
});
|
||||
|
||||
test('it renders', function(assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
|
||||
this.render(hbs`{{tag-list}}`);
|
||||
|
||||
assert.equal(
|
||||
this.$()
|
||||
.text()
|
||||
.trim(),
|
||||
''
|
||||
);
|
||||
|
||||
// Template block usage:
|
||||
this.render(hbs`
|
||||
{{#tag-list}}
|
||||
{{/tag-list}}
|
||||
`);
|
||||
|
||||
assert.equal(
|
||||
this.$()
|
||||
.text()
|
||||
.trim(),
|
||||
''
|
||||
);
|
||||
});
|
|
@ -2,18 +2,8 @@ export default function(visitable, attribute, collection, text, filter) {
|
|||
return {
|
||||
visit: visitable('/:dc/services/:service'),
|
||||
externalSource: attribute('data-test-external-source', 'h1 span'),
|
||||
nodes: collection('[data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
}),
|
||||
healthy: collection('[data-test-healthy] [data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
address: text('header strong'),
|
||||
id: text('header em'),
|
||||
}),
|
||||
unhealthy: collection('[data-test-unhealthy] [data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
address: text('header strong'),
|
||||
id: text('header em'),
|
||||
instances: collection('#instances [data-test-tabular-row]', {
|
||||
address: text('[data-test-address]'),
|
||||
}),
|
||||
filter: filter,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Adapter | proxy', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let adapter = this.owner.lookup('adapter:proxy');
|
||||
assert.ok(adapter);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import { moduleFor, test } from 'ember-qunit';
|
||||
|
||||
moduleFor('controller:dc/services/instance', 'Unit | Controller | dc/services/instance', {
|
||||
// Specify the other units that are required for this test.
|
||||
// needs: ['controller:foo']
|
||||
});
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let controller = this.subject();
|
||||
assert.ok(controller);
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Model | proxy', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = run(() => store.createRecord('proxy', {}));
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { moduleFor, test } from 'ember-qunit';
|
||||
|
||||
moduleFor('route:dc/services/instance', 'Unit | Route | dc/services/instance', {
|
||||
// Specify the other units that are required for this test.
|
||||
needs: ['service:repository/service', 'service:repository/proxy'],
|
||||
});
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.subject();
|
||||
assert.ok(route);
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Serializer | proxy', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('proxy');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function(assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = run(() => store.createRecord('proxy', {}));
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue