Skip to content

Commit

Permalink
Nomad Services: job routes, model, and serializer updates (#14226)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
philrenaud committed Sep 7, 2022
1 parent 534869e commit 90fbaa1
Show file tree
Hide file tree
Showing 37 changed files with 478 additions and 19 deletions.
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,
});
}
}
}
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/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 level=@service.level}}>{{@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

0 comments on commit 90fbaa1

Please sign in to comment.