diff --git a/ui/app/adapters/watchable.js b/ui/app/adapters/watchable.js index 29d001630..09efef4e8 100644 --- a/ui/app/adapters/watchable.js +++ b/ui/app/adapters/watchable.js @@ -4,6 +4,7 @@ import { makeArray } from '@ember/array'; import { inject as service } from '@ember/service'; import queryString from 'npm:query-string'; import ApplicationAdapter from './application'; +import { AbortError } from 'ember-data/adapters/errors'; export default ApplicationAdapter.extend({ watchList: service(), @@ -40,6 +41,11 @@ export default ApplicationAdapter.extend({ return this.ajax(url, 'GET', { data: params, + }).catch(error => { + if (error instanceof AbortError) { + return []; + } + throw error; }); }, @@ -53,6 +59,11 @@ export default ApplicationAdapter.extend({ return this.ajax(url, 'GET', { data: params, + }).catch(error => { + if (error instanceof AbortError) { + return {}; + } + throw error; }); }, @@ -76,16 +87,24 @@ export default ApplicationAdapter.extend({ return this.ajax(url, 'GET', { data: params, - }).then(json => { - this.get('store').pushPayload(relationship.type, { - [relationship.type]: makeArray(json), - }); - }); + }).then( + json => { + this.get('store').pushPayload(relationship.type, { + [relationship.type]: makeArray(json), + }); + }, + error => { + if (error instanceof AbortError) { + return relationship.kind === 'belongsTo' ? {} : []; + } + throw error; + } + ); } }, handleResponse(status, headers, payload, requestData) { - const newIndex = headers['x-nomad-index']; + const newIndex = headers['X-Nomad-Index']; if (newIndex) { this.get('watchList').setIndexFor(requestData.url, newIndex); } diff --git a/ui/app/services/watch-list.js b/ui/app/services/watch-list.js index f22ad34d9..7e9277dd8 100644 --- a/ui/app/services/watch-list.js +++ b/ui/app/services/watch-list.js @@ -2,13 +2,17 @@ import { readOnly } from '@ember/object/computed'; import { copy } from '@ember/object/internals'; import Service from '@ember/service'; -const list = {}; +let list = {}; export default Service.extend({ list: readOnly(function() { return copy(list, true); }), + init() { + list = {}; + }, + getIndexFor(url) { return list[url] || 0; }, diff --git a/ui/tests/unit/adapters/job-test.js b/ui/tests/unit/adapters/job-test.js index 191d6a96a..1240c2281 100644 --- a/ui/tests/unit/adapters/job-test.js +++ b/ui/tests/unit/adapters/job-test.js @@ -1,4 +1,7 @@ +import { run } from '@ember/runloop'; +import { assign } from '@ember/polyfills'; import { test, moduleFor } from 'ember-qunit'; +import wait from 'ember-test-helpers/wait'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; moduleFor('adapter:job', 'Unit | Adapter | Job', { @@ -80,3 +83,194 @@ test('When a token is set in the token service, then x-nomad-token header is set 'The token header is present on both job requests' ); }); + +test('findAll can be watched', function(assert) { + const { pretender } = this.server; + + const request = () => + this.subject().findAll(null, { modelName: 'job' }, null, { + reload: true, + adapterOptions: { watch: true }, + }); + + request(); + assert.equal( + pretender.handledRequests[0].url, + '/v1/namespaces', + 'First request is for namespaces' + ); + assert.equal( + pretender.handledRequests[1].url, + '/v1/jobs?index=0', + 'Second request is a blocking request for jobs' + ); + + return wait().then(() => { + request(); + assert.equal( + pretender.handledRequests[2].url, + '/v1/jobs?index=1', + 'Third request is a blocking request with an incremented index param' + ); + + return wait(); + }); +}); + +test('findRecord can be watched', function(assert) { + const jobId = JSON.stringify(['job-1', 'default']); + const { pretender } = this.server; + + const request = () => + this.subject().findRecord(null, { modelName: 'job' }, jobId, { + reload: true, + adapterOptions: { watch: true }, + }); + + request(); + assert.equal( + pretender.handledRequests[0].url, + '/v1/namespaces', + 'First request is for namespaces' + ); + assert.equal( + pretender.handledRequests[1].url, + '/v1/job/job-1?index=0', + 'Second request is a blocking request for job-1' + ); + + return wait().then(() => { + request(); + assert.equal( + pretender.handledRequests[2].url, + '/v1/job/job-1?index=1', + 'Third request is a blocking request with an incremented index param' + ); + + return wait(); + }); +}); + +test('relationships can be reloaded', function(assert) { + const { pretender } = this.server; + const plainId = 'job-1'; + const mockModel = makeMockModel(plainId); + + this.subject().reloadRelationship(mockModel, 'summary'); + assert.equal( + pretender.handledRequests[0].url, + `/v1/job/${plainId}/summary`, + 'Relationship was reloaded' + ); +}); + +test('relationship reloads can be watched', function(assert) { + const { pretender } = this.server; + const plainId = 'job-1'; + const mockModel = makeMockModel(plainId); + + this.subject().reloadRelationship(mockModel, 'summary', true); + assert.equal( + pretender.handledRequests[0].url, + '/v1/job/job-1/summary?index=0', + 'First request is a blocking request for job-1 summary relationship' + ); + + return wait().then(() => { + this.subject().reloadRelationship(mockModel, 'summary', true); + assert.equal( + pretender.handledRequests[1].url, + '/v1/job/job-1/summary?index=1', + 'Second request is a blocking request with an incremented index param' + ); + }); +}); + +test('findAll can be canceled', function(assert) { + const { pretender } = this.server; + pretender.get('/v1/jobs', () => [200, {}, '[]'], true); + + this.subject().findAll(null, { modelName: 'job' }, null, { + reload: true, + adapterOptions: { watch: true }, + }); + + const { request: xhr } = pretender.requestReferences[0]; + assert.equal(xhr.status, 0, 'Request is still pending'); + + // Schedule the cancelation before waiting + run.next(() => { + this.subject().cancelFindAll('job'); + }); + + return wait().then(() => { + assert.ok(xhr.aborted, 'Request was aborted'); + }); +}); + +test('findRecord can be canceled', function(assert) { + const { pretender } = this.server; + const jobId = JSON.stringify(['job-1', 'default']); + + pretender.get('/v1/job/:id', () => [200, {}, '{}'], true); + + this.subject().findRecord(null, { modelName: 'job' }, jobId, { + reload: true, + adapterOptions: { watch: true }, + }); + + const { request: xhr } = pretender.requestReferences[0]; + assert.equal(xhr.status, 0, 'Request is still pending'); + + // Schedule the cancelation before waiting + run.next(() => { + this.subject().cancelFindRecord('job', jobId); + }); + + return wait().then(() => { + assert.ok(xhr.aborted, 'Request was aborted'); + }); +}); + +test('relationship reloads can be canceled', function(assert) { + const { pretender } = this.server; + const plainId = 'job-1'; + const mockModel = makeMockModel(plainId); + pretender.get('/v1/job/:id/summary', () => [200, {}, '{}'], true); + + this.subject().reloadRelationship(mockModel, 'summary', true); + + const { request: xhr } = pretender.requestReferences[0]; + assert.equal(xhr.status, 0, 'Request is still pending'); + + // Schedule the cancelation before waiting + run.next(() => { + this.subject().cancelReloadRelationship(mockModel, 'summary'); + }); + + return wait().then(() => { + assert.ok(xhr.aborted, 'Request was aborted'); + }); +}); + +function makeMockModel(id, options) { + return assign( + { + relationshipFor(name) { + return { + kind: 'belongsTo', + type: 'job-summary', + key: name, + }; + }, + belongsTo(name) { + return { + link() { + return `/v1/job/${id}/${name}`; + }, + }; + }, + }, + options + ); +}