diff --git a/ui-v2/app/computed/catchable.js b/ui-v2/app/computed/catchable.js new file mode 100644 index 0000000000..9cc2bf008b --- /dev/null +++ b/ui-v2/app/computed/catchable.js @@ -0,0 +1,11 @@ +import ComputedProperty from '@ember/object/computed'; +import computedFactory from 'consul-ui/utils/computed/factory'; + +export default class Catchable extends ComputedProperty { + catch(cb) { + return this.meta({ + catch: cb, + }); + } +} +export const computed = computedFactory(Catchable); diff --git a/ui-v2/app/controllers/dc/services/show.js b/ui-v2/app/controllers/dc/services/show.js index 3f21a7b36b..8182d66203 100644 --- a/ui-v2/app/controllers/dc/services/show.js +++ b/ui-v2/app/controllers/dc/services/show.js @@ -1,9 +1,13 @@ import Controller from '@ember/controller'; import { get, set, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import WithSearching from 'consul-ui/mixins/with-searching'; -export default Controller.extend(WithSearching, { +import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source'; +export default Controller.extend(WithEventSource, WithSearching, { dom: service('dom'), + notify: service('flashMessages'), + items: alias('item.Nodes'), init: function() { this.searchParams = { serviceInstance: 's', @@ -17,6 +21,19 @@ export default Controller.extend(WithSearching, { // need this variable set(this, 'selectedTab', 'instances'); }, + item: listen('item').catch(function(e) { + if (e.target.readyState === 1) { + // OPEN + if (get(e, 'error.errors.firstObject.status') === '404') { + get(this, 'notify').add({ + destroyOnClick: false, + sticky: true, + type: 'warning', + action: 'update', + }); + } + } + }), searchable: computed('items', function() { return get(this, 'searchables.serviceInstance') .add(get(this, 'items')) diff --git a/ui-v2/app/instance-initializers/event-source.js b/ui-v2/app/instance-initializers/event-source.js index 705accdd5b..01265dfec5 100644 --- a/ui-v2/app/instance-initializers/event-source.js +++ b/ui-v2/app/instance-initializers/event-source.js @@ -34,6 +34,12 @@ export function initialize(container) { repo: 'repository/service/event-source', }, }, + { + route: 'dc/services/show', + services: { + repo: 'repository/service/event-source', + }, + }, ]) .forEach(function(definition) { if (typeof definition.extend !== 'undefined') { diff --git a/ui-v2/app/mixins/with-event-source.js b/ui-v2/app/mixins/with-event-source.js index 1fccd1dc83..f315f32f0b 100644 --- a/ui-v2/app/mixins/with-event-source.js +++ b/ui-v2/app/mixins/with-event-source.js @@ -1,12 +1,38 @@ import Mixin from '@ember/object/mixin'; +import { computed as catchable } from 'consul-ui/computed/catchable'; +import purify from 'consul-ui/utils/computed/purify'; -export default Mixin.create({ +import WithListeners from 'consul-ui/mixins/with-listeners'; +const PREFIX = '_'; +export default Mixin.create(WithListeners, { + setProperties: function(model) { + const _model = {}; + Object.keys(model).forEach(key => { + // here (see comment below on deleting) + if (typeof this[key] !== 'undefined' && this[key].isDescriptor) { + _model[`${PREFIX}${key}`] = model[key]; + const meta = this.constructor.metaForProperty(key) || {}; + if (typeof meta.catch === 'function') { + if (typeof _model[`${PREFIX}${key}`].addEventListener === 'function') { + this.listen(_model[`_${key}`], 'error', meta.catch.bind(this)); + } + } + } else { + _model[key] = model[key]; + } + }); + return this._super(_model); + }, reset: function(exiting) { if (exiting) { Object.keys(this).forEach(prop => { if (this[prop] && typeof this[prop].close === 'function') { this[prop].close(); // ember doesn't delete on 'resetController' by default + // right now we only call reset when we are exiting, therefore a full + // setProperties will be called the next time we enter the Route so this + // is ok for what we need and means that the above conditional works + // as expected (see 'here' comment above) delete this[prop]; } }); @@ -14,3 +40,6 @@ export default Mixin.create({ return this._super(...arguments); }, }); +export const listen = purify(catchable, function(props) { + return props.map(item => `${PREFIX}${item}`); +}); diff --git a/ui-v2/app/models/service.js b/ui-v2/app/models/service.js index cf98df3114..3d7e425928 100644 --- a/ui-v2/app/models/service.js +++ b/ui-v2/app/models/service.js @@ -30,6 +30,7 @@ export default Model.extend({ Node: attr(), Service: attr(), Checks: attr(), + meta: attr(), passing: computed('ChecksPassing', 'Checks', function() { let num = 0; // TODO: use typeof diff --git a/ui-v2/app/routes/dc/services/show.js b/ui-v2/app/routes/dc/services/show.js index 3757421cd8..670c607669 100644 --- a/ui-v2/app/routes/dc/services/show.js +++ b/ui-v2/app/routes/dc/services/show.js @@ -15,14 +15,6 @@ export default Route.extend({ const repo = get(this, 'repo'); return hash({ item: repo.findBySlug(params.name, this.modelFor('dc').dc.Name), - }).then(function(model) { - return { - ...model, - ...{ - // Nodes happen to be the ServiceInstances here - items: model.item.Nodes, - }, - }; }); }, setupController: function(controller, model) { diff --git a/ui-v2/app/serializers/application.js b/ui-v2/app/serializers/application.js index 8150179254..defe01efe8 100644 --- a/ui-v2/app/serializers/application.js +++ b/ui-v2/app/serializers/application.js @@ -15,15 +15,6 @@ export default Serializer.extend({ const headers = payload[HTTP_HEADERS_SYMBOL] || {}; delete payload[HTTP_HEADERS_SYMBOL]; const normalizedPayload = this.normalizePayload(payload, id, requestType); - const response = this._super( - store, - primaryModelClass, - { - [primaryModelClass.modelName]: normalizedPayload, - }, - id, - requestType - ); // put the meta onto the response, here this is ok // as JSON-API allows this and our specific data is now in // response[primaryModelClass.modelName] @@ -31,7 +22,7 @@ export default Serializer.extend({ // (which was the reason for the Symbol-like property earlier) // use a method modelled on ember-data methods so we have the opportunity to // do this on a per-model level - response.meta = this.normalizeMeta( + const meta = this.normalizeMeta( store, primaryModelClass, headers, @@ -39,7 +30,19 @@ export default Serializer.extend({ id, requestType ); - return response; + if (requestType === 'queryRecord') { + normalizedPayload.meta = meta; + } + return this._super( + store, + primaryModelClass, + { + meta: meta, + [primaryModelClass.modelName]: normalizedPayload, + }, + id, + requestType + ); }, normalizeMeta: function(store, primaryModelClass, headers, payload, id, requestType) { const meta = { diff --git a/ui-v2/app/services/repository/service.js b/ui-v2/app/services/repository/service.js index 61ef1d61cd..3e42c84f56 100644 --- a/ui-v2/app/services/repository/service.js +++ b/ui-v2/app/services/repository/service.js @@ -8,6 +8,18 @@ export default RepositoryService.extend({ findBySlug: function(slug, dc) { return this._super(...arguments).then(function(item) { const nodes = get(item, 'Nodes'); + if (nodes.length === 0) { + // TODO: Add an store.error("404", "message") or similar + // or move all this to serializer + const e = new Error(); + e.errors = [ + { + status: '404', + title: 'Not found', + }, + ]; + throw e; + } const service = get(nodes, 'firstObject'); const tags = nodes .reduce(function(prev, item) { @@ -16,6 +28,7 @@ export default RepositoryService.extend({ .uniq(); set(service, 'Tags', tags); set(service, 'Nodes', nodes); + set(service, 'meta', get(item, 'meta')); return service; }); }, @@ -36,6 +49,7 @@ export default RepositoryService.extend({ return service; } // TODO: Add an store.error("404", "message") or similar + // or move all this to serializer const e = new Error(); e.errors = [ { diff --git a/ui-v2/app/templates/dc/services/-notifications.hbs b/ui-v2/app/templates/dc/services/-notifications.hbs new file mode 100644 index 0000000000..9dee0a3ec9 --- /dev/null +++ b/ui-v2/app/templates/dc/services/-notifications.hbs @@ -0,0 +1,7 @@ +{{#if (eq type 'update')}} + {{#if (eq status 'warning') }} + This service has been deregistered and no longer exists in the catalog. + {{else}} + {{/if}} +{{/if}} + diff --git a/ui-v2/app/templates/dc/services/index.hbs b/ui-v2/app/templates/dc/services/index.hbs index 25885f12e5..ccc7e02e7e 100644 --- a/ui-v2/app/templates/dc/services/index.hbs +++ b/ui-v2/app/templates/dc/services/index.hbs @@ -1,7 +1,6 @@ {{#app-view class="service list"}} - {{!TODO: Look at the item passed through to figure what partial to show, also move into its own service partial, for the moment keeping here for visibility}} {{#block-slot 'notification' as |status type|}} - {{partial 'dc/acls/notifications'}} + {{partial 'dc/services/notifications'}} {{/block-slot}} {{#block-slot 'header'}}