Commit cc0ee668 authored by Phil Renaud's avatar Phil Renaud Committed by Phil Renaud
Browse files

Nomad Services: job routes, model, and serializer updates (#14226)

* Added to subnav and basic table implemented

* Existing services become service fragments, and services tab aggregated beneath job route

* Index page within jobs/job/services

* Watchable services

* Lintfixes

* Links to clients and individual services set up

* Child service route

* Keyboard shortcuts on service page

* Model that shows consul services as well, plus level and provider cols

* lintfix

* Level as query param

* Watch job for service name changes too

* Lintfix

* Testfixes

* Placeholder mirage route
parent 91e7d849
Showing with 223 additions and 18 deletions
+223 -18
import Watchable from './watchable';
import classic from 'ember-classic-decorator';
@classic
export default class ServiceAdapter extends Watchable {}
......@@ -143,9 +143,9 @@ export default class Watchable extends ApplicationAdapter {
reloadRelationship(
model,
relationshipName,
options = { watch: false, abortController: null }
options = { watch: false, abortController: null, replace: false }
) {
const { watch, abortController } = options;
const { watch, abortController, replace } = options;
const relationship = model.relationshipFor(relationshipName);
if (relationship.kind !== 'belongsTo' && relationship.kind !== 'hasMany') {
throw new Error(
......@@ -185,6 +185,9 @@ export default class Watchable extends ApplicationAdapter {
modelClass,
json
);
if (replace) {
store.unloadAll(relationship.type);
}
store.push(normalizedData);
},
(error) => {
......
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
export default class JobServiceRowComponent extends Component {
@service router;
@action
gotoService(service) {
if (service.provider === 'nomad') {
this.router.transitionTo('jobs.job.services.service', service.name, {
queryParams: { level: service.level },
instances: service.instances,
});
}
}
}
import Controller from '@ember/controller';
export default class JobsJobServicesController extends Controller {}
import Controller from '@ember/controller';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
import { union } from '@ember/object/computed';
export default class JobsJobServicesIndexController extends Controller.extend(
WithNamespaceResetting
) {
@alias('model') job;
@alias('job.taskGroups') taskGroups;
@computed('taskGroups.@each.tasks')
get tasks() {
return this.taskGroups.map((group) => group.tasks.toArray()).flat();
}
@computed('tasks.@each.services')
get taskServices() {
return this.tasks
.map((t) => (t.services || []).toArray())
.flat()
.compact()
.map((service) => {
service.level = 'task';
return service;
});
}
@computed('model.taskGroup.services.@each.name', 'taskGroups')
get groupServices() {
return this.taskGroups
.map((g) => (g.services || []).toArray())
.flat()
.compact()
.map((service) => {
service.level = 'group';
return service;
});
}
@union('taskServices', 'groupServices') serviceFragments;
// Services, grouped by name, with aggregatable allocations.
@computed(
'job.services.@each.{name,allocation}',
'job.services.length',
'serviceFragments'
)
get services() {
return this.serviceFragments.map((fragment) => {
fragment.instances = this.job.services.filter(
(s) => s.name === fragment.name && s.derivedLevel === fragment.level
);
return fragment;
});
}
}
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
export default class JobsJobServicesServiceController extends Controller {
@service router;
queryParams = ['level'];
@action
gotoAllocation(allocation) {
this.router.transitionTo('allocations.allocation', allocation.get('id'));
}
}
......@@ -141,6 +141,7 @@ export default class Job extends Model {
@hasMany('variables') variables;
@belongsTo('namespace') namespace;
@belongsTo('job-scale') scaleState;
@hasMany('services') services;
@hasMany('recommendation-summary') recommendationSummaries;
......
import { attr } from '@ember-data/model';
import Fragment from 'ember-data-model-fragments/fragment';
import { fragment } from 'ember-data-model-fragments/attributes';
export default class Service extends Fragment {
@attr('string') name;
@attr('string') portLabel;
@attr() tags;
@attr('string') onUpdate;
@attr('string') provider;
@fragment('consul-connect') connect;
}
import { attr } from '@ember-data/model';
import Fragment from 'ember-data-model-fragments/fragment';
import { fragment } from 'ember-data-model-fragments/attributes';
// @ts-check
import { attr, belongsTo } from '@ember-data/model';
import Model from '@ember-data/model';
import { alias } from '@ember/object/computed';
export default class Service extends Fragment {
@attr('string') name;
@attr('string') portLabel;
export default class Service extends Model {
@belongsTo('allocation') allocation;
@belongsTo('job') job;
@belongsTo('node') node;
@attr('string') address;
@attr('number') createIndex;
@attr('string') datacenter;
@attr('number') modifyIndex;
@attr('string') namespace;
@attr('number') port;
@attr('string') serviceName;
@attr() tags;
@attr('string') onUpdate;
@fragment('consul-connect') connect;
@alias('serviceName') name;
// Services can exist at either Group or Task level.
// While our endpoints to get them do not explicitly tell us this,
// we can infer it from the service's ID:
get derivedLevel() {
const idWithoutServiceName = this.id.replace(this.serviceName, '');
if (idWithoutServiceName.includes('group-')) {
return 'group';
} else {
return 'task';
}
}
}
......@@ -35,7 +35,7 @@ export default class TaskGroup extends Fragment {
@fragmentArray('task') tasks;
@fragmentArray('service') services;
@fragmentArray('service-fragment') services;
@fragmentArray('volume-definition') volumes;
......
......@@ -48,7 +48,7 @@ export default class Task extends Fragment {
@attr('number') reservedCPU;
@attr('number') reservedDisk;
@attr('number') reservedEphemeralDisk;
@fragmentArray('service') services;
@fragmentArray('service-fragment') services;
@fragmentArray('volume-mount', { defaultValue: () => [] }) volumeMounts;
......
......@@ -24,6 +24,9 @@ Router.map(function () {
this.route('evaluations');
this.route('allocations');
this.route('clients');
this.route('services', function () {
this.route('service', { path: '/:name' });
});
});
});
......
import Route from '@ember/routing/route';
import WithWatchers from 'nomad-ui/mixins/with-watchers';
import { collect } from '@ember/object/computed';
import {
watchRecord,
watchRelationship,
} from 'nomad-ui/utils/properties/watch';
export default class JobsJobServicesRoute extends Route.extend(WithWatchers) {
model() {
const job = this.modelFor('jobs.job');
return job && job.get('services').then(() => job);
}
startWatchers(controller, model) {
if (model) {
controller.set('watchServices', this.watchServices.perform(model));
controller.set('watchJob', this.watchJob.perform(model));
}
}
@watchRelationship('services', true) watchServices;
@watchRecord('job') watchJob;
@collect('watchServices', 'watchJob') watchers;
}
import Route from '@ember/routing/route';
export default class JobsJobServicesIndexRoute extends Route {}
import Route from '@ember/routing/route';
export default class JobsJobServicesServiceRoute extends Route {
model({ name = '', level = '' }) {
const services = this.modelFor('jobs.job')
.get('services')
.filter(
(service) => service.name === name && service.derivedLevel === level
);
return { name, instances: services || [] };
}
}
......@@ -101,6 +101,11 @@ export default class JobSerializer extends ApplicationSerializer {
related: buildURL(`${jobURL}/evaluations`, { namespace }),
},
},
services: {
links: {
related: buildURL(`${jobURL}/services`, { namespace }),
},
},
variables: {
links: {
related: buildURL(`/${apiNamespace}/vars`, {
......
import ApplicationSerializer from './application';
import classic from 'ember-classic-decorator';
@classic
export default class ServiceFragmentSerializer extends ApplicationSerializer {
attrs = {
connect: 'Connect',
};
arrayNullOverrides = ['Tags'];
}
import ApplicationSerializer from './application';
import classic from 'ember-classic-decorator';
import ApplicationSerializer from './application';
@classic
export default class ServiceSerializer extends ApplicationSerializer {
attrs = {
connect: 'Connect',
};
arrayNullOverrides = ['Tags'];
normalize(typeHash, hash) {
hash.AllocationID = hash.AllocID; // TODO: keyForRelationship maybe?
hash.JobID = JSON.stringify([hash.JobID, hash.Namespace]);
return super.normalize(typeHash, hash);
}
}
......@@ -48,3 +48,4 @@
@import './components/evaluations';
@import './components/variables';
@import './components/keyboard-shortcuts-modal';
@import './components/services';
.service-list {
.title {
.back-link {
text-decoration: none;
color: #363636;
position: relative;
top: 4px;
}
}
}
\ No newline at end of file
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