diff --git a/ui/packages/consul-ui/app/adapters/http.js b/ui/packages/consul-ui/app/adapters/http.js index 3e0f38fd4..3794e27a5 100644 --- a/ui/packages/consul-ui/app/adapters/http.js +++ b/ui/packages/consul-ui/app/adapters/http.js @@ -17,11 +17,11 @@ import AdapterError, { // the naming of things (serialized vs query etc) const read = function(adapter, modelName, type, query = {}) { return adapter.rpc( - function(adapter, request, query) { - return adapter[`requestFor${type}`](request, query); + function(adapter, ...rest) { + return adapter[`requestFor${type}`](...rest); }, - function(serializer, respond, query) { - return serializer[`respondFor${type}`](respond, query); + function(serializer, ...rest) { + return serializer[`respondFor${type}`](...rest); }, query, modelName @@ -29,11 +29,11 @@ const read = function(adapter, modelName, type, query = {}) { }; const write = function(adapter, modelName, type, snapshot) { return adapter.rpc( - function(adapter, request, serialized, unserialized) { - return adapter[`requestFor${type}`](request, serialized, unserialized); + function(adapter, ...rest) { + return adapter[`requestFor${type}`](...rest); }, - function(serializer, respond, serialized, unserialized) { - return serializer[`respondFor${type}`](respond, serialized, unserialized); + function(serializer, ...rest) { + return serializer[`respondFor${type}`](...rest); }, snapshot, modelName @@ -49,6 +49,7 @@ export default class HttpAdapter extends Adapter { let unserialized, serialized; const serializer = store.serializerFor(modelName); + const modelClass = store.modelFor(modelName); // workable way to decide whether this is a snapshot // essentially 'is attributable'. // Snapshot is private so we can't do instanceof here @@ -66,14 +67,14 @@ export default class HttpAdapter extends Adapter { return client .request(function(request) { - return req(adapter, request, serialized, unserialized); + return req(adapter, request, serialized, unserialized, modelClass); }) .catch(function(e) { return adapter.error(e); }) .then(function(respond) { // TODO: When HTTPAdapter:responder changes, this will also need to change - return resp(serializer, respond, serialized, unserialized); + return resp(serializer, respond, serialized, unserialized, modelClass); }); // TODO: Potentially add specific serializer errors here // .catch(function(e) { diff --git a/ui/packages/consul-ui/app/components/consul/service-instance/list/index.hbs b/ui/packages/consul-ui/app/components/consul/service-instance/list/index.hbs index 603b29c09..a47ebfa47 100644 --- a/ui/packages/consul-ui/app/components/consul/service-instance/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/service-instance/list/index.hbs @@ -5,8 +5,8 @@ as |item index|> {{#if (eq @routeName "dc.services.show")}} - - {{item.ID}} + + {{item.Service.ID}} {{else}} @@ -15,9 +15,9 @@ as |item index|> {{/if}} - {{#if @checks}} - - + {{#if @node}} + + {{else}} @@ -35,7 +35,7 @@ as |item index|> {{/if}} -{{#if (gt item.Node.Node.length 0)}} +{{#if (not @node)}}
@@ -63,24 +63,6 @@ as |item index|>
{{/if}} -{{#if (and @checks item.Port)}} -
-
- Port -
-
- - :{{item.Port}} -
-
-{{/if}} -{{#if checks}} - -{{else}} -{{/if}}
\ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/consul/service-instance/search-bar/index.hbs b/ui/packages/consul-ui/app/components/consul/service-instance/search-bar/index.hbs index d4d44b52b..71fb5c27e 100644 --- a/ui/packages/consul-ui/app/components/consul/service-instance/search-bar/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/service-instance/search-bar/index.hbs @@ -21,13 +21,9 @@ {{#let components.Optgroup components.Option as |Optgroup Option|}} - - - - - - - + {{#each @searchproperties as |prop|}} + + {{/each}} {{/let}} diff --git a/ui/packages/consul-ui/app/controllers/dc/nodes/show/services.js b/ui/packages/consul-ui/app/controllers/dc/nodes/show/services.js deleted file mode 100644 index 0fb98b410..000000000 --- a/ui/packages/consul-ui/app/controllers/dc/nodes/show/services.js +++ /dev/null @@ -1,28 +0,0 @@ -import Controller from '@ember/controller'; -import { get, computed } from '@ember/object'; - -export default class ServicesController extends Controller { - queryParams = { - search: { - as: 'filter', - replace: true, - }, - }; - - @computed('item.Checks.[]') - get checks() { - const checks = {}; - get(this, 'item.Checks') - .filter(item => { - return item.ServiceID !== ''; - }) - .forEach(item => { - if (typeof checks[item.ServiceID] === 'undefined') { - checks[item.ServiceID] = []; - } - checks[item.ServiceID].push(item); - }); - - return checks; - } -} diff --git a/ui/packages/consul-ui/app/models/node.js b/ui/packages/consul-ui/app/models/node.js index 8d2892abd..2c77cd979 100644 --- a/ui/packages/consul-ui/app/models/node.js +++ b/ui/packages/consul-ui/app/models/node.js @@ -1,4 +1,4 @@ -import Model, { attr } from '@ember-data/model'; +import Model, { attr, hasMany } from '@ember-data/model'; import { computed } from '@ember/object'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; @@ -18,7 +18,7 @@ export default class Node extends Model { @attr() meta; // {} @attr() Meta; // {} @attr() TaggedAddresses; // {lan, wan} - @attr() Services; // ServiceInstances[] + @hasMany('service-instance') Services; // TODO: Rename to ServiceInstances @fragmentArray('health-check') Checks; @computed('Checks.[]', 'ChecksCritical', 'ChecksPassing', 'ChecksWarning') diff --git a/ui/packages/consul-ui/app/routes/dc/nodes/show/services.js b/ui/packages/consul-ui/app/routes/dc/nodes/show/services.js index 5dbd9b712..dd97616e5 100644 --- a/ui/packages/consul-ui/app/routes/dc/nodes/show/services.js +++ b/ui/packages/consul-ui/app/routes/dc/nodes/show/services.js @@ -2,6 +2,13 @@ import Route from 'consul-ui/routing/route'; export default class ServicesRoute extends Route { queryParams = { + sortBy: 'sort', + status: 'status', + source: 'source', + searchproperty: { + as: 'searchproperty', + empty: [['Name', 'Tags', 'ID', 'Address', 'Port', 'Service.Meta']], + }, search: { as: 'filter', replace: true, @@ -13,7 +20,10 @@ export default class ServicesRoute extends Route { .split('.') .slice(0, -1) .join('.'); - return this.modelFor(parent); + return { + ...this.modelFor(parent), + searchProperties: this.queryParams.searchproperty.empty[0], + }; } setupController(controller, model) { diff --git a/ui/packages/consul-ui/app/routes/dc/services/show/instances.js b/ui/packages/consul-ui/app/routes/dc/services/show/instances.js index a8b7a1c80..6690bd457 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show/instances.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show/instances.js @@ -20,7 +20,10 @@ export default class InstancesRoute extends Route { .split('.') .slice(0, -1) .join('.'); - return this.modelFor(parent); + return { + ...this.modelFor(parent), + searchProperties: this.queryParams.searchproperty.empty[0], + }; } setupController(controller, model) { diff --git a/ui/packages/consul-ui/app/serializers/http.js b/ui/packages/consul-ui/app/serializers/http.js index 2821d468a..831aeb1ce 100644 --- a/ui/packages/consul-ui/app/serializers/http.js +++ b/ui/packages/consul-ui/app/serializers/http.js @@ -1,6 +1,14 @@ import Serializer from '@ember-data/serializer/rest'; export default class HttpSerializer extends Serializer { + transformBelongsToResponse(store, relationship, parent, item) { + return item; + } + + transformHasManyResponse(store, relationship, parent, item) { + return item; + } + respondForQuery(respond, query) { return respond((headers, body) => body); } diff --git a/ui/packages/consul-ui/app/serializers/node.js b/ui/packages/consul-ui/app/serializers/node.js index ef028c736..e65a45cda 100644 --- a/ui/packages/consul-ui/app/serializers/node.js +++ b/ui/packages/consul-ui/app/serializers/node.js @@ -1,5 +1,7 @@ import Serializer from './application'; +import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/node'; +import { classify } from '@ember/string'; // TODO: Looks like ID just isn't used at all consider just using .Node for // the SLUG_KEY @@ -10,25 +12,70 @@ const fillSlug = function(item) { return item; }; -export default class NodeSerializer extends Serializer { +export default class NodeSerializer extends Serializer.extend(EmbeddedRecordsMixin) { primaryKey = PRIMARY_KEY; slugKey = SLUG_KEY; - respondForQuery(respond, query) { - return super.respondForQuery( + attrs = { + Services: { + embedded: 'always', + }, + }; + + transformHasManyResponse(store, relationship, item, parent = null) { + switch (relationship.key) { + case 'Services': + const checks = {}; + (item.Checks || []) + .filter(item => { + return item.ServiceID !== ''; + }) + .forEach(item => { + if (typeof checks[item.ServiceID] === 'undefined') { + checks[item.ServiceID] = []; + } + checks[item.ServiceID].push(item); + }); + const serializer = this.store.serializerFor(relationship.type); + item.Services = item.Services.map(service => + serializer.transformHasManyResponseFromNode(item, service, checks) + ); + return item; + } + return super.transformHasManyResponse(...arguments); + } + + respondForQuery(respond, query, data, modelClass) { + const body = super.respondForQuery( cb => respond((headers, body) => cb(headers, body.map(fillSlug))), query ); + modelClass.eachRelationship((key, relationship) => { + body.forEach(item => + this[`transform${classify(relationship.kind)}Response`]( + this.store, + relationship, + item, + body + ) + ); + }); + return body; } - respondForQueryRecord(respond, query) { - return super.respondForQueryRecord( + respondForQueryRecord(respond, query, data, modelClass) { + const body = super.respondForQueryRecord( cb => respond((headers, body) => { return cb(headers, fillSlug(body)); }), query ); + + modelClass.eachRelationship((key, relationship) => { + this[`transform${classify(relationship.kind)}Response`](this.store, relationship, body); + }); + return body; } respondForQueryLeader(respond, query) { diff --git a/ui/packages/consul-ui/app/serializers/service-instance.js b/ui/packages/consul-ui/app/serializers/service-instance.js index e1b0400a0..9aa47222b 100644 --- a/ui/packages/consul-ui/app/serializers/service-instance.js +++ b/ui/packages/consul-ui/app/serializers/service-instance.js @@ -5,9 +5,62 @@ export default class ServiceInstanceSerializer extends Serializer { primaryKey = PRIMARY_KEY; slugKey = SLUG_KEY; + hash = JSON.stringify; + + extractUid(item) { + return this.hash([ + item.Namespace || 'default', + item.Datacenter, + item.Node.Node, + item.Service.ID, + ]); + } + + transformHasManyResponseFromNode(node, item, checks) { + const serviceChecks = checks[item.ID] || []; + const statuses = serviceChecks.reduce( + (prev, item) => { + switch (item.Status) { + case 'passing': + prev.ChecksPassing.push(item); + break; + case 'warning': + prev.ChecksWarning.push(item); + break; + case 'critical': + prev.ChecksCritical.push(item); + break; + } + return prev; + }, + { + ChecksPassing: [], + ChecksWarning: [], + ChecksCritical: [], + } + ); + const instance = { + ...statuses, + Service: item, + Checks: serviceChecks, + Node: { + Datacenter: node.Datacenter, + Namespace: node.Namespace, + ID: node.ID, + Node: node.Node, + Address: node.Address, + TaggedAddresses: node.TaggedAddresses, + Meta: node.Meta, + }, + }; + + instance.uid = this.extractUid(instance); + return instance; + } + respondForQuery(respond, query) { - return super.respondForQuery(function(cb) { - return respond(function(headers, body) { + const body = super.respondForQuery(cb => { + return respond((headers, body) => { if (body.length === 0) { const e = new Error(); e.errors = [ @@ -18,14 +71,25 @@ export default class ServiceInstanceSerializer extends Serializer { ]; throw e; } + body.forEach(item => { + item.Datacenter = query.dc; + item.Namespace = query.ns || 'default'; + item.uid = this.extractUid(item); + }); return cb(headers, body); }); }, query); + return body; } respondForQueryRecord(respond, query) { - return super.respondForQueryRecord(function(cb) { - return respond(function(headers, body) { + return super.respondForQueryRecord(cb => { + return respond((headers, body) => { + body.forEach(item => { + item.Datacenter = query.dc; + item.Namespace = query.ns || 'default'; + item.uid = this.extractUid(item); + }); body = body.find(function(item) { return item.Node.Node === query.node && item.Service.ID === query.serviceId; }); diff --git a/ui/packages/consul-ui/app/templates/dc/nodes/show/services.hbs b/ui/packages/consul-ui/app/templates/dc/nodes/show/services.hbs index 76c563841..462308ec7 100644 --- a/ui/packages/consul-ui/app/templates/dc/nodes/show/services.hbs +++ b/ui/packages/consul-ui/app/templates/dc/nodes/show/services.hbs @@ -1,19 +1,51 @@ -{{#let item.Services as |items|}} -
+{{#let (hash + statuses=(if status (split status ',') undefined) + sources=(if source (split source ',') undefined) + searchproperties=(if (not-eq searchproperty undefined) + (split searchproperty ',') + searchProperties + ) +) as |filters|}} + {{#let (or sortBy "Status:asc") as |sort|}} + {{#let item.Services as |items|}} +
{{#if (gt items.length 0) }} - - {{/if}} - - - - - + @searchproperties={{searchProperties}} + + @sort={{sort}} + @onsort={{action (mut sortBy) value="target.selected"}} + + @filter={{filters}} + @onfilter={{hash + searchproperty=(action (mut searchproperty) value="target.selectedItems") + status=(action (mut status) value="target.selectedItems") + source=(action (mut source) value="target.selectedItems") + }} + /> + {{/if}} + {{! filter out any sidecar proxies }} + + + + +

@@ -21,8 +53,10 @@

-
-
+ +
+ {{/let}} + {{/let}} {{/let}} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs b/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs index b786066d7..e66d998a3 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs @@ -5,7 +5,7 @@ sources=(if source (split source ',') undefined) searchproperties=(if (not-eq searchproperty undefined) (split searchproperty ',') - (array 'Name' 'Tags' 'ID' 'Address' 'Port' 'Service.Meta' 'Node.Meta') + searchProperties ) ) as |filters|}} {{#let (or sortBy "Status:asc") as |sort|}} @@ -15,6 +15,7 @@ @sources={{externalSources}} @search={{search}} @onsearch={{action (mut search) value="target.value"}} + @searchproperties={{searchProperties}} @sort={{sort}} @onsort={{action (mut sortBy) value="target.selected"}} @@ -27,6 +28,7 @@ }} /> {{/if}} + {{! Service > Service Instance view doesn't require filtering of proxies }} - Object.assign({}, item, { - Datacenter: dc, - // TODO: default isn't required here, once we've - // refactored out our Serializer this can go - Namespace: nspace, - uid: `["${nspace}","${dc}","${item.ID}"]`, - }) - ); const actual = serializer.respondForQuery( function(cb) { const headers = {}; @@ -33,13 +27,22 @@ module('Integration | Serializer | node', function(hooks) { }, { dc: dc, - } + }, + { + dc: dc, + }, + modelClass ); - assert.deepEqual(actual, expected); + assert.equal(actual[0].Datacenter, dc); + assert.equal(actual[0].Namespace, nspace); + assert.equal(actual[0].uid, `["${nspace}","${dc}","${actual[0].ID}"]`); }); }); test('respondForQueryRecord returns the correct data for item endpoint', function(assert) { + const store = this.owner.lookup('service:store'); const serializer = this.owner.lookup('serializer:node'); + serializer.store = store; + const modelClass = store.modelFor('node'); const dc = 'dc-1'; const id = 'node-name'; const request = { @@ -65,9 +68,15 @@ module('Integration | Serializer | node', function(hooks) { }, { dc: dc, - } + }, + { + dc: dc, + }, + modelClass ); - assert.deepEqual(actual, expected); + assert.equal(actual.Datacenter, dc); + assert.equal(actual.Namespace, nspace); + assert.equal(actual.uid, `["${nspace}","${dc}","${actual.ID}"]`); }); }); test('respondForQueryLeader returns the correct data', function(assert) {