Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 15 commits into from
Aug 26, 2022
Merged
5 changes: 5 additions & 0 deletions ui/app/adapters/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Watchable from './watchable';
import classic from 'ember-classic-decorator';

@classic
export default class ServiceAdapter extends Watchable {}
7 changes: 5 additions & 2 deletions ui/app/adapters/watchable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -185,6 +185,9 @@ export default class Watchable extends ApplicationAdapter {
modelClass,
json
);
if (replace) {
store.unloadAll(relationship.type);
}
store.push(normalizedData);
},
(error) => {
Expand Down
17 changes: 17 additions & 0 deletions ui/app/components/job-service-row.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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,
});
}
}
}
2 changes: 1 addition & 1 deletion ui/app/controllers/allocations/allocation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default class IndexController extends Controller.extend(Sortable) {
@computed('[email protected]')
get taskServices() {
return this.get('tasks')
.map((t) => (t && t.get('services') || []).toArray())
.map((t) => ((t && t.get('services')) || []).toArray())
.flat()
.compact();
}
Expand Down
3 changes: 3 additions & 0 deletions ui/app/controllers/jobs/job/services.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Controller from '@ember/controller';

export default class JobsJobServicesController extends Controller {}
58 changes: 58 additions & 0 deletions ui/app/controllers/jobs/job/services/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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('[email protected]')
get tasks() {
return this.taskGroups.map((group) => group.tasks.toArray()).flat();
}

@computed('[email protected]')
get taskServices() {
return this.tasks
.map((t) => (t.services || []).toArray())
.flat()
.compact()
.map((service) => {
service.level = 'task';
return service;
});
}

@computed('[email protected]', '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;
});
}
}
13 changes: 13 additions & 0 deletions ui/app/controllers/jobs/job/services/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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'));
}
}
1 change: 1 addition & 0 deletions ui/app/models/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
12 changes: 12 additions & 0 deletions ui/app/models/service-fragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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;
}
38 changes: 30 additions & 8 deletions ui/app/models/service.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
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';
}
}
}
2 changes: 1 addition & 1 deletion ui/app/models/task-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default class TaskGroup extends Fragment {

@fragmentArray('task') tasks;

@fragmentArray('service') services;
@fragmentArray('service-fragment') services;

@fragmentArray('volume-definition') volumes;

Expand Down
2 changes: 1 addition & 1 deletion ui/app/models/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
});
});
});

Expand Down
26 changes: 26 additions & 0 deletions ui/app/routes/jobs/job/services.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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;
}
3 changes: 3 additions & 0 deletions ui/app/routes/jobs/job/services/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Route from '@ember/routing/route';

export default class JobsJobServicesIndexRoute extends Route {}
12 changes: 12 additions & 0 deletions ui/app/routes/jobs/job/services/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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 || [] };
}
}
5 changes: 5 additions & 0 deletions ui/app/serializers/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`, {
Expand Down
11 changes: 11 additions & 0 deletions ui/app/serializers/service-fragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ApplicationSerializer from './application';
import classic from 'ember-classic-decorator';

@classic
export default class ServiceFragmentSerializer extends ApplicationSerializer {
attrs = {
connect: 'Connect',
};

arrayNullOverrides = ['Tags'];
}
12 changes: 6 additions & 6 deletions ui/app/serializers/service.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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);
}
}
1 change: 1 addition & 0 deletions ui/app/styles/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@
@import './components/evaluations';
@import './components/secure-variables';
@import './components/keyboard-shortcuts-modal';
@import './components/services';
10 changes: 10 additions & 0 deletions ui/app/styles/components/services.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.service-list {
.title {
.back-link {
text-decoration: none;
color: #363636;
position: relative;
top: 4px;
}
}
}
39 changes: 39 additions & 0 deletions ui/app/templates/components/job-service-row.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<tr {{on "click" (fn this.gotoService @service)}} class={{if (eq @service.provider "nomad") "is-interactive"}} data-test-service={{@service.id}}>
<td>
{{#if (eq @service.provider "nomad")}}
<FlightIcon @name="nomad-color" />
{{else if (eq @service.provider "consul")}}
<FlightIcon @name="consul-color" />
{{/if}}
</td>
<td
{{keyboard-shortcut
enumerated=true
action=(action "gotoService" @service)
}}
>
{{#if (eq @service.provider "nomad")}}
<LinkTo class="is-primary" @route="jobs.job.services.service" @model={{@service}} @query={{hash [email protected]}}>{{@service.name}}</LinkTo>
{{else}}
{{@service.name}}
{{/if}}
</td>
<td>
{{@service.level}}
</td>
<td>
<LinkTo @route="clients.client" @model={{@service.instances.0.node.id}}>{{@service.instances.0.node.name}}</LinkTo>
</td>
<td>
{{#each @service.tags as |tag|}}
<span class="tag">{{tag}}</span>
{{/each}}
</td>
<td>
{{#if (eq @service.provider "nomad")}}
{{@service.instances.length}} {{pluralize "allocation" @service.instances.length}}
{{else}}
--
{{/if}}
</td>
</tr>
9 changes: 9 additions & 0 deletions ui/app/templates/components/job-subnav.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,14 @@
</LinkTo>
</li>
{{/if}}
<li data-test-tab="services">
<LinkTo
@route="jobs.job.services"
@model={{@job}}
@activeClass="is-active"
>
Services
</LinkTo>
</li>
</ul>
</div>
3 changes: 3 additions & 0 deletions ui/app/templates/jobs/job/services.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{page-title "Job " @model.name " services"}}
<JobSubnav @job={{@model}} />
{{outlet}}
Loading