Skip to content

Commit

Permalink
UI: Service Instances (#5326)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
johncowen authored and John Cowen committed May 1, 2019
1 parent cfa4bc2 commit 355f034
Show file tree
Hide file tree
Showing 75 changed files with 918 additions and 369 deletions.
20 changes: 20 additions & 0 deletions ui-v2/app/adapters/proxy.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
4 changes: 4 additions & 0 deletions ui-v2/app/components/healthcheck-info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});
36 changes: 36 additions & 0 deletions ui-v2/app/components/healthcheck-list.js
Original file line number Diff line number Diff line change
@@ -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;
},
},
});
5 changes: 5 additions & 0 deletions ui-v2/app/components/healthcheck-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Component from '@ember/component';

export default Component.extend({
classNames: ['healthcheck-output'],
});
11 changes: 9 additions & 2 deletions ui-v2/app/components/healthcheck-status.js
Original file line number Diff line number Diff line change
@@ -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;
}),
});
1 change: 1 addition & 0 deletions ui-v2/app/components/tab-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import Component from '@ember/component';
export default Component.extend({
name: 'tab',
tagName: 'nav',
classNames: ['tab-nav'],
});
6 changes: 6 additions & 0 deletions ui-v2/app/components/tag-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from '@ember/component';

export default Component.extend({
tagName: 'dl',
classNames: ['tag-list'],
});
17 changes: 17 additions & 0 deletions ui-v2/app/controllers/dc/services/instance.js
Original file line number Diff line number Diff line change
@@ -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);
},
},
});
57 changes: 29 additions & 28 deletions ui-v2/app/controllers/dc/services/show.js
Original file line number Diff line number Diff line change
@@ -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));
}),
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;
});
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));
}),
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();
}
});
},
},
});
3 changes: 1 addition & 2 deletions ui-v2/app/initializers/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
Expand Down
12 changes: 12 additions & 0 deletions ui-v2/app/models/proxy.js
Original file line number Diff line number Diff line change
@@ -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'),
});
3 changes: 3 additions & 0 deletions ui-v2/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const routes = {
show: {
_options: { path: '/:name' },
},
instance: {
_options: { path: '/:name/:id' },
},
},
// Nodes represent a consul node
nodes: {
Expand Down
29 changes: 29 additions & 0 deletions ui-v2/app/routes/dc/services/instance.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
1 change: 1 addition & 0 deletions ui-v2/app/routes/dc/services/show.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default Route.extend({
return {
...model,
...{
// Nodes happen to be the ServiceInstances here
items: model.item.Nodes,
},
};
Expand Down
6 changes: 6 additions & 0 deletions ui-v2/app/serializers/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Serializer from './application';
import { PRIMARY_KEY } from 'consul-ui/models/proxy';

export default Serializer.extend({
primaryKey: PRIMARY_KEY,
});
4 changes: 3 additions & 1 deletion ui-v2/app/services/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
33 changes: 33 additions & 0 deletions ui-v2/app/services/repository/proxy.js
Original file line number Diff line number Diff line change
@@ -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;
});
},
});
39 changes: 29 additions & 10 deletions ui-v2/app/services/repository/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?
});
},
});
9 changes: 9 additions & 0 deletions ui-v2/app/styles/components/app-view/layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 355f034

Please sign in to comment.