Unverified Commit 2f93deae authored by Michael Lange's avatar Michael Lange Committed by GitHub
Browse files

Merge pull request #3893 from hashicorp/f-ui-polling-foundation

UI: Background Polling Step 1 - Foundational support & jobs
parents 67b99d44 421f082a
Showing with 562 additions and 133 deletions
+562 -133
......@@ -34,6 +34,24 @@ export default RESTAdapter.extend({
});
},
// In order to remove stale records from the store, findHasMany has to unload
// all records related to the request in question.
findHasMany(store, snapshot, link, relationship) {
return this._super(...arguments).then(payload => {
const ownerType = snapshot.modelName;
const relationshipType = relationship.type;
// Naively assume that the inverse relationship is named the same as the
// owner type. In the event it isn't, findHasMany should be overridden.
store
.peekAll(relationshipType)
.filter(record => record.get(`${ownerType}.id`) === snapshot.id)
.forEach(record => {
store.unloadRecord(record);
});
return payload;
});
},
// Single record requests deviate from REST practice by using
// the singular form of the resource name.
//
......
import { inject as service } from '@ember/service';
import RSVP from 'rsvp';
import { assign } from '@ember/polyfills';
import ApplicationAdapter from './application';
import Watchable from './watchable';
export default ApplicationAdapter.extend({
export default Watchable.extend({
system: service(),
shouldReloadAll: () => true,
buildQuery() {
const namespace = this.get('system.activeNamespace.id');
if (namespace && namespace !== 'default') {
return { namespace };
}
return {};
},
findAll() {
......@@ -26,28 +24,26 @@ export default ApplicationAdapter.extend({
});
},
findRecord(store, { modelName }, id, snapshot) {
// To make a findRecord response reflect the findMany response, the JobSummary
// from /summary needs to be stitched into the response.
findRecordSummary(modelName, name, snapshot, namespaceQuery) {
return this.ajax(`${this.buildURL(modelName, name, snapshot, 'findRecord')}/summary`, 'GET', {
data: assign(this.buildQuery() || {}, namespaceQuery),
});
},
// URL is the form of /job/:name?namespace=:namespace with arbitrary additional query params
const [name, namespace] = JSON.parse(id);
findRecord(store, type, id, snapshot) {
const [, namespace] = JSON.parse(id);
const namespaceQuery = namespace && namespace !== 'default' ? { namespace } : {};
return RSVP.hash({
job: this.ajax(this.buildURL(modelName, name, snapshot, 'findRecord'), 'GET', {
data: assign(this.buildQuery() || {}, namespaceQuery),
}),
summary: this.ajax(
`${this.buildURL(modelName, name, snapshot, 'findRecord')}/summary`,
'GET',
{
data: assign(this.buildQuery() || {}, namespaceQuery),
}
),
}).then(({ job, summary }) => {
job.JobSummary = summary;
return job;
});
return this._super(store, type, id, snapshot, namespaceQuery);
},
urlForFindRecord(id, type, hash) {
const [name, namespace] = JSON.parse(id);
let url = this._super(name, type, hash);
if (namespace && namespace !== 'default') {
url += `?namespace=${namespace}`;
}
return url;
},
findAllocations(job) {
......@@ -60,19 +56,17 @@ export default ApplicationAdapter.extend({
},
fetchRawDefinition(job) {
const [name, namespace] = JSON.parse(job.get('id'));
const namespaceQuery = namespace && namespace !== 'default' ? { namespace } : {};
const url = this.buildURL('job', name, job, 'findRecord');
return this.ajax(url, 'GET', { data: assign(this.buildQuery() || {}, namespaceQuery) });
const url = this.buildURL('job', job.get('id'), job, 'findRecord');
return this.ajax(url, 'GET', { data: this.buildQuery() });
},
forcePeriodic(job) {
if (job.get('periodic')) {
const [name, namespace] = JSON.parse(job.get('id'));
let url = `${this.buildURL('job', name, job, 'findRecord')}/periodic/force`;
const [path, params] = this.buildURL('job', job.get('id'), job, 'findRecord').split('?');
let url = `${path}/periodic/force`;
if (namespace) {
url += `?namespace=${namespace}`;
if (params) {
url += `?${params}`;
}
return this.ajax(url, 'POST');
......
import { get, computed } from '@ember/object';
import { assign } from '@ember/polyfills';
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(),
store: service(),
xhrs: computed(function() {
return {};
}),
ajaxOptions(url) {
const ajaxOptions = this._super(...arguments);
const previousBeforeSend = ajaxOptions.beforeSend;
ajaxOptions.beforeSend = function(jqXHR) {
if (previousBeforeSend) {
previousBeforeSend(...arguments);
}
this.get('xhrs')[url] = jqXHR;
jqXHR.always(() => {
delete this.get('xhrs')[url];
});
};
return ajaxOptions;
},
findAll(store, type, sinceToken, snapshotRecordArray, additionalParams = {}) {
const params = assign(this.buildQuery(), additionalParams);
const url = this.urlForFindAll(type.modelName);
if (get(snapshotRecordArray || {}, 'adapterOptions.watch')) {
params.index = this.get('watchList').getIndexFor(url);
}
return this.ajax(url, 'GET', {
data: params,
}).catch(error => {
if (error instanceof AbortError) {
return [];
}
throw error;
});
},
findRecord(store, type, id, snapshot, additionalParams = {}) {
let [url, params] = this.buildURL(type.modelName, id, snapshot, 'findRecord').split('?');
params = assign(queryString.parse(params) || {}, this.buildQuery(), additionalParams);
if (get(snapshot || {}, 'adapterOptions.watch')) {
params.index = this.get('watchList').getIndexFor(url);
}
return this.ajax(url, 'GET', {
data: params,
}).catch(error => {
if (error instanceof AbortError) {
return {};
}
throw error;
});
},
reloadRelationship(model, relationshipName, watch = false) {
const relationship = model.relationshipFor(relationshipName);
if (relationship.kind !== 'belongsTo' && relationship.kind !== 'hasMany') {
throw new Error(
`${relationship.key} must be a belongsTo or hasMany, instead it was ${relationship.kind}`
);
} else {
const url = model[relationship.kind](relationship.key).link();
let params = {};
if (watch) {
params.index = this.get('watchList').getIndexFor(url);
}
if (url.includes('?')) {
params = assign(queryString.parse(url.split('?')[1]), params);
}
return this.ajax(url, 'GET', {
data: params,
}).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'];
if (newIndex) {
this.get('watchList').setIndexFor(requestData.url, newIndex);
}
return this._super(...arguments);
},
cancelFindRecord(modelName, id) {
const url = this.urlForFindRecord(id, modelName);
const xhr = this.get('xhrs')[url];
if (xhr) {
xhr.abort();
}
},
cancelFindAll(modelName) {
const xhr = this.get('xhrs')[this.urlForFindAll(modelName)];
if (xhr) {
xhr.abort();
}
},
cancelReloadRelationship(model, relationshipName) {
const relationship = model.relationshipFor(relationshipName);
if (relationship.kind !== 'belongsTo' && relationship.kind !== 'hasMany') {
throw new Error(
`${relationship.key} must be a belongsTo or hasMany, instead it was ${relationship.kind}`
);
} else {
const url = model[relationship.kind](relationship.key).link();
const xhr = this.get('xhrs')[url];
if (xhr) {
xhr.abort();
}
}
},
});
import Component from '@ember/component';
import { computed } from '@ember/object';
import { computed, observer } from '@ember/object';
import { run } from '@ember/runloop';
import { assign } from '@ember/polyfills';
import { guidFor } from '@ember/object/internals';
import { guidFor, copy } from '@ember/object/internals';
import d3 from 'npm:d3-selection';
import 'npm:d3-transition';
import WindowResizable from '../mixins/window-resizable';
......@@ -23,7 +23,7 @@ export default Component.extend(WindowResizable, {
maskId: null,
_data: computed('data', function() {
const data = this.get('data');
const data = copy(this.get('data'), true);
const sum = data.mapBy('value').reduce(sumAggregate, 0);
return data.map(({ label, value, className, layers }, index) => ({
......@@ -66,6 +66,10 @@ export default Component.extend(WindowResizable, {
this.renderChart();
},
updateChart: observer('_data.@each.{value,label,className}', function() {
this.renderChart();
}),
// prettier-ignore
/* eslint-disable */
renderChart() {
......@@ -73,7 +77,7 @@ export default Component.extend(WindowResizable, {
const width = this.$('svg').width();
const filteredData = _data.filter(d => d.value > 0);
let slices = chart.select('.bars').selectAll('g').data(filteredData);
let slices = chart.select('.bars').selectAll('g').data(filteredData, d => d.label);
let sliceCount = filteredData.length;
slices.exit().remove();
......@@ -82,7 +86,8 @@ export default Component.extend(WindowResizable, {
.append('g')
.on('mouseenter', d => {
run(() => {
const slice = slices.filter(datum => datum === d);
const slices = this.get('slices');
const slice = slices.filter(datum => datum.label === d.label);
slices.classed('active', false).classed('inactive', true);
slice.classed('active', true).classed('inactive', false);
this.set('activeDatum', d);
......@@ -99,7 +104,15 @@ export default Component.extend(WindowResizable, {
});
slices = slices.merge(slicesEnter);
slices.attr('class', d => d.className || `slice-${_data.indexOf(d)}`);
slices.attr('class', d => {
const className = d.className || `slice-${_data.indexOf(d)}`
const activeDatum = this.get('activeDatum');
const isActive = activeDatum && activeDatum.label === d.label;
const isInactive = activeDatum && activeDatum.label !== d.label;
return [ className, isActive && 'active', isInactive && 'inactive' ].compact().join(' ');
});
this.set('slices', slices);
const setWidth = d => `${width * d.percent - (d.index === sliceCount - 1 || d.index === 0 ? 1 : 2)}px`
const setOffset = d => `${width * d.offset + (d.index === 0 ? 0 : 1)}px`
......@@ -117,7 +130,6 @@ export default Component.extend(WindowResizable, {
.attr('width', setWidth)
.attr('x', setOffset)
let layers = slices.selectAll('.bar').data((d, i) => {
return new Array(d.layers || 1).fill(assign({ index: i }, d));
});
......
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { lazyClick } from '../helpers/lazy-click';
import { watchRelationship } from 'nomad-ui/utils/properties/watch';
export default Component.extend({
store: service(),
tagName: 'tr',
classNames: ['job-row', 'is-interactive'],
......@@ -17,7 +21,16 @@ export default Component.extend({
// Reload the job in order to get detail information
const job = this.get('job');
if (job && !job.get('isLoading')) {
job.reload();
job.reload().then(() => {
this.get('watch').perform(job, 100);
});
}
},
willDestroy() {
this.get('watch').cancelAll();
this._super(...arguments);
},
watch: watchRelationship('summary'),
});
import Component from '@ember/component';
import { computed } from '@ember/object';
import { run } from '@ember/runloop';
import { copy } from '@ember/object/internals';
import JSONFormatterPkg from 'npm:json-formatter-js';
// json-formatter-js is packaged in a funny way that ember-cli-browserify
......@@ -14,7 +15,7 @@ export default Component.extend({
expandDepth: Infinity,
formatter: computed('json', 'expandDepth', function() {
return new JSONFormatter(this.get('json'), this.get('expandDepth'), {
return new JSONFormatter(copy(this.get('json'), true), this.get('expandDepth'), {
theme: 'nomad',
});
}),
......
import { collect, sum } from '@ember/object/computed';
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo } from 'ember-data/relationships';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
import sumAggregation from '../utils/properties/sum-aggregation';
export default Model.extend({
job: belongsTo('job'),
taskGroupSummaries: fragmentArray('task-group-summary'),
// Aggregate allocation counts across all summaries
queuedAllocs: sumAggregation('taskGroupSummaries', 'queuedAllocs'),
startingAllocs: sumAggregation('taskGroupSummaries', 'startingAllocs'),
runningAllocs: sumAggregation('taskGroupSummaries', 'runningAllocs'),
completeAllocs: sumAggregation('taskGroupSummaries', 'completeAllocs'),
failedAllocs: sumAggregation('taskGroupSummaries', 'failedAllocs'),
lostAllocs: sumAggregation('taskGroupSummaries', 'lostAllocs'),
allocsList: collect(
'queuedAllocs',
'startingAllocs',
'runningAllocs',
'completeAllocs',
'failedAllocs',
'lostAllocs'
),
totalAllocs: sum('allocsList'),
pendingChildren: attr('number'),
runningChildren: attr('number'),
deadChildren: attr('number'),
childrenList: collect('pendingChildren', 'runningChildren', 'deadChildren'),
totalChildren: sum('childrenList'),
});
import { collect, sum, bool, equal, or } from '@ember/object/computed';
import { alias, bool, equal, or } from '@ember/object/computed';
import { computed } from '@ember/object';
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo, hasMany } from 'ember-data/relationships';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
import sumAggregation from '../utils/properties/sum-aggregation';
const JOB_TYPES = ['service', 'batch', 'system'];
......@@ -83,34 +82,21 @@ export default Model.extend({
datacenters: attr(),
taskGroups: fragmentArray('task-group', { defaultValue: () => [] }),
taskGroupSummaries: fragmentArray('task-group-summary'),
// Aggregate allocation counts across all summaries
queuedAllocs: sumAggregation('taskGroupSummaries', 'queuedAllocs'),
startingAllocs: sumAggregation('taskGroupSummaries', 'startingAllocs'),
runningAllocs: sumAggregation('taskGroupSummaries', 'runningAllocs'),
completeAllocs: sumAggregation('taskGroupSummaries', 'completeAllocs'),
failedAllocs: sumAggregation('taskGroupSummaries', 'failedAllocs'),
lostAllocs: sumAggregation('taskGroupSummaries', 'lostAllocs'),
allocsList: collect(
'queuedAllocs',
'startingAllocs',
'runningAllocs',
'completeAllocs',
'failedAllocs',
'lostAllocs'
),
totalAllocs: sum('allocsList'),
pendingChildren: attr('number'),
runningChildren: attr('number'),
deadChildren: attr('number'),
childrenList: collect('pendingChildren', 'runningChildren', 'deadChildren'),
totalChildren: sum('childrenList'),
summary: belongsTo('job-summary'),
// Alias through to the summary, as if there was no relationship
taskGroupSummaries: alias('summary.taskGroupSummaries'),
queuedAllocs: alias('summary.queuedAllocs'),
startingAllocs: alias('summary.startingAllocs'),
runningAllocs: alias('summary.runningAllocs'),
completeAllocs: alias('summary.completeAllocs'),
failedAllocs: alias('summary.failedAllocs'),
lostAllocs: alias('summary.lostAllocs'),
totalAllocs: alias('summary.totalAllocs'),
pendingChildren: alias('summary.pendingChildren'),
runningChildren: alias('summary.runningChildren'),
deadChildren: alias('summary.deadChildren'),
totalChildren: alias('summary.childrenList'),
version: attr('number'),
......
......@@ -4,6 +4,8 @@ import attr from 'ember-data/attr';
import { fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes';
import sumAggregation from '../utils/properties/sum-aggregation';
const maybe = arr => arr || [];
export default Fragment.extend({
job: fragmentOwner(),
......@@ -13,7 +15,7 @@ export default Fragment.extend({
tasks: fragmentArray('task'),
allocations: computed('job.allocations.@each.taskGroup', function() {
return this.get('job.allocations').filterBy('taskGroupName', this.get('name'));
return maybe(this.get('job.allocations')).filterBy('taskGroupName', this.get('name'));
}),
reservedCPU: sumAggregation('tasks', 'reservedCPU'),
......@@ -32,6 +34,6 @@ export default Fragment.extend({
}),
summary: computed('job.taskGroupSummaries.[]', function() {
return this.get('job.taskGroupSummaries').findBy('name', this.get('name'));
return maybe(this.get('job.taskGroupSummaries')).findBy('name', this.get('name'));
}),
});
......@@ -3,6 +3,7 @@ import Route from '@ember/routing/route';
import { run } from '@ember/runloop';
import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
import notifyForbidden from 'nomad-ui/utils/notify-forbidden';
import { watchAll } from 'nomad-ui/utils/properties/watch';
export default Route.extend(WithForbiddenState, {
system: service(),
......@@ -35,9 +36,18 @@ export default Route.extend(WithForbiddenState, {
setupController(controller) {
this.syncToController(controller);
controller.set('modelWatch', this.get('watch').perform());
return this._super(...arguments);
},
deactivate() {
this.get('watch').cancelAll();
this._super(...arguments);
},
watch: watchAll('job'),
actions: {
refreshRoute() {
this.refresh();
......
import { inject as service } from '@ember/service';
import { collect } from '@ember/object/computed';
import Route from '@ember/routing/route';
import RSVP from 'rsvp';
import notifyError from 'nomad-ui/utils/notify-error';
import { watchRecord, watchRelationship } from 'nomad-ui/utils/properties/watch';
export default Route.extend({
store: service(),
token: service(),
watchList: service(),
serialize(model) {
return { job_name: model.get('plainId') };
......@@ -21,4 +25,29 @@ export default Route.extend({
})
.catch(notifyError(this));
},
setupController(controller, model) {
controller.set('watchers', {
model: this.get('watch').perform(model),
summary: this.get('watchSummary').perform(model),
evaluations: this.get('watchEvaluations').perform(model),
deployments: this.get('watchDeployments').perform(model),
});
return this._super(...arguments);
},
deactivate() {
this.get('allWatchers').forEach(watcher => {
watcher.cancelAll();
});
this._super(...arguments);
},
watch: watchRecord('job'),
watchSummary: watchRelationship('summary'),
watchEvaluations: watchRelationship('evaluations'),
watchDeployments: watchRelationship('deployments'),
allWatchers: collect('watch', 'watchSummary', 'watchEvaluations', 'watchDeployments'),
});
import { copy } from '@ember/object/internals';
import { get } from '@ember/object';
import { makeArray } from '@ember/array';
import JSONSerializer from 'ember-data/serializers/json';
......@@ -35,9 +37,33 @@ export default JSONSerializer.extend({
documentHash.included.push(...included);
}
});
store.push(documentHash);
});
store.push(documentHash);
},
normalizeFindAllResponse(store, modelClass) {
const result = this._super(...arguments);
this.cullStore(store, modelClass.modelName, result.data);
return result;
},
// When records are removed server-side, and therefore don't show up in requests,
// the local copies of those records need to be unloaded from the store.
cullStore(store, type, records, storeFilter = () => true) {
const newRecords = copy(records).filter(record => get(record, 'id'));
const oldRecords = store.peekAll(type);
oldRecords
.filter(record => get(record, 'id'))
.filter(storeFilter)
.forEach(old => {
const newRecord = newRecords.find(record => get(record, 'id') === get(old, 'id'));
if (!newRecord) {
store.unloadRecord(old);
} else {
newRecords.removeObject(newRecord);
}
});
},
modelNameFromPayloadKey(key) {
......
import { get } from '@ember/object';
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
normalize(modelClass, hash) {
// Transform the map-based Summary object into an array-based
// TaskGroupSummary fragment list
hash.PlainJobId = hash.JobID;
hash.ID = JSON.stringify([hash.JobID, hash.Namespace || 'default']);
hash.TaskGroupSummaries = Object.keys(get(hash, 'Summary') || {}).map(key => {
const allocStats = get(hash, `Summary.${key}`) || {};
const summary = { Name: key };
Object.keys(allocStats).forEach(
allocKey => (summary[`${allocKey}Allocs`] = allocStats[allocKey])
);
return summary;
});
// Lift the children stats out of the Children object
const childrenStats = get(hash, 'Children');
if (childrenStats) {
Object.keys(childrenStats).forEach(
childrenKey => (hash[`${childrenKey}Children`] = childrenStats[childrenKey])
);
}
return this._super(modelClass, hash);
},
});
import { get } from '@ember/object';
import { assign } from '@ember/polyfills';
import ApplicationSerializer from './application';
import queryString from 'npm:query-string';
......@@ -34,27 +33,6 @@ export default ApplicationSerializer.extend({
hash.ParameterizedJob = true;
}
// Transform the map-based JobSummary object into an array-based
// JobSummary fragment list
hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary') || {}).map(key => {
const allocStats = get(hash, `JobSummary.Summary.${key}`) || {};
const summary = { Name: key };
Object.keys(allocStats).forEach(
allocKey => (summary[`${allocKey}Allocs`] = allocStats[allocKey])
);
return summary;
});
// Lift the children stats out of the JobSummary object
const childrenStats = get(hash, 'JobSummary.Children');
if (childrenStats) {
Object.keys(childrenStats).forEach(
childrenKey => (hash[`${childrenKey}Children`] = childrenStats[childrenKey])
);
}
return this._super(typeHash, hash);
},
......@@ -63,11 +41,17 @@ export default ApplicationSerializer.extend({
!hash.NamespaceID || hash.NamespaceID === 'default' ? undefined : hash.NamespaceID;
const { modelName } = modelClass;
const jobURL = this.store
const [jobURL] = this.store
.adapterFor(modelName)
.buildURL(modelName, hash.PlainId, hash, 'findRecord');
.buildURL(modelName, hash.ID, hash, 'findRecord')
.split('?');
return assign(this._super(...arguments), {
summary: {
links: {
related: buildURL(`${jobURL}/summary`, { namespace: namespace }),
},
},
allocations: {
links: {
related: buildURL(`${jobURL}/allocations`, { namespace: namespace }),
......
import { readOnly } from '@ember/object/computed';
import { copy } from '@ember/object/internals';
import Service from '@ember/service';
let list = {};
export default Service.extend({
list: readOnly(function() {
return copy(list, true);
}),
init() {
list = {};
},
getIndexFor(url) {
return list[url] || 0;
},
setIndexFor(url, value) {
list[url] = value;
},
});
......@@ -5,10 +5,10 @@
<td data-test-job-type>{{job.displayType}}</td>
<td data-test-job-priority>{{job.priority}}</td>
<td data-test-job-task-groups>
{{#if job.isReloading}}
...
{{else}}
{{#if job.taskGroups.length}}
{{job.taskGroups.length}}
{{else}}
--
{{/if}}
</td>
<td data-test-job-allocations>
......
import Ember from 'ember';
import { get } from '@ember/object';
import RSVP from 'rsvp';
import { task } from 'ember-concurrency';
import wait from 'nomad-ui/utils/wait';
export function watchRecord(modelName) {
return task(function*(id, throttle = 2000) {
if (typeof id === 'object') {
id = get(id, 'id');
}
while (!Ember.testing) {
try {
yield RSVP.all([
this.get('store').findRecord(modelName, id, {
reload: true,
adapterOptions: { watch: true },
}),
wait(throttle),
]);
} catch (e) {
yield e;
break;
} finally {
this.get('store')
.adapterFor(modelName)
.cancelFindRecord(modelName, id);
}
}
}).drop();
}
export function watchRelationship(relationshipName) {
return task(function*(model, throttle = 2000) {
while (!Ember.testing) {
try {
yield RSVP.all([
this.get('store')
.adapterFor(model.constructor.modelName)
.reloadRelationship(model, relationshipName, true),
wait(throttle),
]);
} catch (e) {
yield e;
break;
} finally {
this.get('store')
.adapterFor(model.constructor.modelName)
.cancelReloadRelationship(model, relationshipName);
}
}
}).drop();
}
export function watchAll(modelName) {
return task(function*(throttle = 2000) {
while (!Ember.testing) {
try {
yield RSVP.all([
this.get('store').findAll(modelName, { reload: true, adapterOptions: { watch: true } }),
wait(throttle),
]);
} catch (e) {
yield e;
break;
} finally {
this.get('store')
.adapterFor(modelName)
.cancelFindAll(modelName);
}
}
}).drop();
}
import RSVP from 'rsvp';
// An always passing promise used to throttle other promises
export default function wait(duration) {
return new RSVP.Promise(resolve => {
setTimeout(() => {
resolve(`Waited ${duration}ms`);
}, duration);
});
}
......@@ -11,41 +11,72 @@ export function findLeader(schema) {
}
export default function() {
const server = this;
this.timing = 0; // delay for each request, automatically set to 0 during testing
this.namespace = 'v1';
this.trackRequests = Ember.testing;
this.get('/jobs', function({ jobs }, { queryParams }) {
const json = this.serialize(jobs.all());
const namespace = queryParams.namespace || 'default';
return json
.filter(
job =>
namespace === 'default'
? !job.NamespaceID || job.NamespaceID === namespace
: job.NamespaceID === namespace
)
.map(job => filterKeys(job, 'TaskGroups', 'NamespaceID'));
});
this.get('/job/:id', function({ jobs }, { params, queryParams }) {
const job = jobs.all().models.find(job => {
const jobIsDefault = !job.namespaceId || job.namespaceId === 'default';
const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default';
return (
job.id === params.id &&
(job.namespaceId === queryParams.namespace || (jobIsDefault && qpIsDefault))
);
});
return job ? this.serialize(job) : new Response(404, {}, null);
});
this.get('/job/:id/summary', function({ jobSummaries }, { params }) {
return this.serialize(jobSummaries.findBy({ jobId: params.id }));
});
const nomadIndices = {}; // used for tracking blocking queries
const server = this;
const withBlockingSupport = function(fn) {
return function(schema, request) {
// Get the original response
let { url } = request;
url = url.replace(/index=\d+[&;]?/, '');
const response = fn.apply(this, arguments);
// Get and increment the approrpriate index
nomadIndices[url] || (nomadIndices[url] = 1);
const index = nomadIndices[url];
nomadIndices[url]++;
// Annotate the response with the index
if (response instanceof Response) {
response.headers['X-Nomad-Index'] = index;
return response;
}
return new Response(200, { 'x-nomad-index': index }, response);
};
};
this.get(
'/jobs',
withBlockingSupport(function({ jobs }, { queryParams }) {
const json = this.serialize(jobs.all());
const namespace = queryParams.namespace || 'default';
return json
.filter(
job =>
namespace === 'default'
? !job.NamespaceID || job.NamespaceID === namespace
: job.NamespaceID === namespace
)
.map(job => filterKeys(job, 'TaskGroups', 'NamespaceID'));
})
);
this.get(
'/job/:id',
withBlockingSupport(function({ jobs }, { params, queryParams }) {
const job = jobs.all().models.find(job => {
const jobIsDefault = !job.namespaceId || job.namespaceId === 'default';
const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default';
return (
job.id === params.id &&
(job.namespaceId === queryParams.namespace || (jobIsDefault && qpIsDefault))
);
});
return job ? this.serialize(job) : new Response(404, {}, null);
})
);
this.get(
'/job/:id/summary',
withBlockingSupport(function({ jobSummaries }, { params }) {
return this.serialize(jobSummaries.findBy({ jobId: params.id }));
})
);
this.get('/job/:id/allocations', function({ allocations }, { params }) {
return this.serialize(allocations.where({ jobId: params.id }));
......
......@@ -15,6 +15,8 @@ export default Factory.extend({
modifyTime: () => faker.date.past(2 / 365, REF_TIME) * 1000000,
namespace: null,
clientStatus: faker.list.random(...CLIENT_STATUSES),
desiredStatus: faker.list.random(...DESIRED_STATUSES),
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment