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

UI: Specialized Job Detail Pages #3829

Merged
merged 34 commits into from
Feb 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
37c39a9
Filter child jobs out of the root jobs list
DingoEatingFuzz Jan 23, 2018
0c50472
Model the parent/child relationship in jobs
DingoEatingFuzz Jan 23, 2018
552b958
Specialized children-status-bar variant of the allocation-status-bar
DingoEatingFuzz Jan 23, 2018
eae275f
Use the children summary instead of alloc summary when applicable
DingoEatingFuzz Jan 23, 2018
525312d
Fix lint-staged paths
DingoEatingFuzz Jan 23, 2018
c6369a6
Computed a template type for a job
DingoEatingFuzz Jan 25, 2018
a47835f
Handle the difference between parameterized on single and list responses
DingoEatingFuzz Jan 25, 2018
ec35b5a
Deconstruct the existing job detail page into common parts
DingoEatingFuzz Jan 25, 2018
bb1e0d6
Recreate the service job detail page using job part components
DingoEatingFuzz Jan 25, 2018
0ea1d0d
Fleshing out job page parts and differences
DingoEatingFuzz Jan 25, 2018
b4bdc61
New job page components for parent jobs and batch jobs
DingoEatingFuzz Jan 25, 2018
ab22e95
Paginated and sortable table for job launches/children jobs
DingoEatingFuzz Jan 25, 2018
81273dc
Bring payload in from the job api response
DingoEatingFuzz Jan 26, 2018
fbd166b
Breadcrumbs for the periodic child job page
DingoEatingFuzz Jan 26, 2018
ec49a72
Elastic mode for cli window component
DingoEatingFuzz Jan 26, 2018
3fc0910
Payload details for the parameterized child job detail page
DingoEatingFuzz Jan 26, 2018
d7b9283
For now, the system job is identical to the service job
DingoEatingFuzz Jan 26, 2018
5238052
Add ability to force a periodic job launch
DingoEatingFuzz Jan 26, 2018
60cb1ac
Differentiate between no search matches and no allocs on task group page
DingoEatingFuzz Jan 26, 2018
dc0fa16
Clean up force launch button
DingoEatingFuzz Jan 26, 2018
d264c43
State periodic or parameterized as the job type when applicable
DingoEatingFuzz Jan 26, 2018
35f388a
Trim the parent job prefix off the child job names when displaying them
DingoEatingFuzz Jan 27, 2018
98b0068
Update job factory to use traits for specifying job type
DingoEatingFuzz Jan 30, 2018
3907f39
Integration tests for the body job part
DingoEatingFuzz Jan 31, 2018
e8606f7
Job part children tests
DingoEatingFuzz Jan 31, 2018
88f1349
Job part evaluations test
DingoEatingFuzz Jan 31, 2018
80c2acd
Running deployment job page part tests
DingoEatingFuzz Jan 31, 2018
3ca0d64
Tests for the placement failures job part
DingoEatingFuzz Feb 1, 2018
82de007
Tests for the summary job page part
DingoEatingFuzz Feb 1, 2018
1671786
Tests for the task groups job page part
DingoEatingFuzz Feb 2, 2018
6e30864
New module-for-job for acceptance testing job detail differences
DingoEatingFuzz Feb 2, 2018
b7f57ec
Integration test for periodic job force launch
DingoEatingFuzz Feb 2, 2018
de27385
Inject system so namespace shows up on job detail components
DingoEatingFuzz Feb 2, 2018
9e606f3
Always shutdown the mirage server
DingoEatingFuzz Feb 2, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions ui/app/adapters/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,17 @@ export default ApplicationAdapter.extend({
const url = this.buildURL('job', name, job, 'findRecord');
return this.ajax(url, 'GET', { data: assign(this.buildQuery() || {}, namespaceQuery) });
},

forcePeriodic(job) {
if (job.get('periodic')) {
const [name, namespace] = JSON.parse(job.get('id'));
let url = `${this.buildURL('job', name, job, 'findRecord')}/periodic/force`;

if (namespace) {
url += `?namespace=${namespace}`;
}

return this.ajax(url, 'POST');
}
},
});
2 changes: 2 additions & 0 deletions ui/app/components/allocation-status-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export default DistributionBar.extend({

allocationContainer: null,

'data-test-allocation-status-bar': true,

data: computed(
'allocationContainer.{queuedAllocs,completeAllocs,failedAllocs,runningAllocs,startingAllocs}',
function() {
Expand Down
27 changes: 27 additions & 0 deletions ui/app/components/children-status-bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { computed } from '@ember/object';
import DistributionBar from './distribution-bar';

export default DistributionBar.extend({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be misreading this, but would it be possible to use composition instead of inheritance here? That is, have this component (and the other subclass) render {{distribution-bar data=data}}?

It would mean rendering another component, but it might be clearer to understand the code that way. It took me a while to figure out how this subclass interacted with the parent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely agree it would be clearer and more idiomatic. I just still have a gut aversion to making extra components. Especially when those components are in lists.

There are no lists in Nomad long enough to cause component allocation to drag the app though, so I shouldn't prematurely optimize.

layoutName: 'components/distribution-bar',

job: null,

'data-test-children-status-bar': true,

data: computed('job.{pendingChildren,runningChildren,deadChildren}', function() {
if (!this.get('job')) {
return [];
}

const children = this.get('job').getProperties(
'pendingChildren',
'runningChildren',
'deadChildren'
);
return [
{ label: 'Pending', value: children.pendingChildren, className: 'queued' },
{ label: 'Running', value: children.runningChildren, className: 'running' },
{ label: 'Dead', value: children.deadChildren, className: 'complete' },
];
}),
});
29 changes: 29 additions & 0 deletions ui/app/components/job-page/abstract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { inject as service } from '@ember/service';

export default Component.extend({
system: service(),

job: null,

// Provide a value that is bound to a query param
sortProperty: null,
sortDescending: null,

// Provide actions that require routing
onNamespaceChange() {},
gotoTaskGroup() {},
gotoJob() {},

breadcrumbs: computed('job.{name,id}', function() {
const job = this.get('job');
return [
{ label: 'Jobs', args: ['jobs'] },
{
label: job.get('name'),
args: ['jobs.job', job],
},
];
}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again wondering if composition is an option here. All of the templates have the same breadcrumbs markup, so if this logic was pulled into a component, there might be no need for an abstract parent class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactoring all of breadcrumbs is in my top three things to do. I'd prefer to leave this the way it is, understanding full well that it is strange and out of place, and clean it up when I refactor all of breadcrumbs.

});
3 changes: 3 additions & 0 deletions ui/app/components/job-page/batch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import AbstractJobPage from './abstract';

export default AbstractJobPage.extend();
16 changes: 16 additions & 0 deletions ui/app/components/job-page/parameterized-child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import PeriodicChildJobPage from './periodic-child';

export default PeriodicChildJobPage.extend({
payload: alias('job.decodedPayload'),
payloadJSON: computed('payload', function() {
let json;
try {
json = JSON.parse(this.get('payload'));
} catch (e) {
// Swallow error and fall back to plain text rendering
}
return json;
}),
});
3 changes: 3 additions & 0 deletions ui/app/components/job-page/parameterized.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import AbstractJobPage from './abstract';

export default AbstractJobPage.extend();
31 changes: 31 additions & 0 deletions ui/app/components/job-page/parts/children.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import Sortable from 'nomad-ui/mixins/sortable';

export default Component.extend(Sortable, {
job: null,

classNames: ['boxed-section'],

// Provide a value that is bound to a query param
sortProperty: null,
sortDescending: null,
currentPage: null,

// Provide an action with access to the router
gotoJob() {},

pageSize: 10,

taskGroups: computed('job.taskGroups.[]', function() {
return this.get('job.taskGroups') || [];
}),

children: computed('job.children.[]', function() {
return this.get('job.children') || [];
}),

listToSort: alias('children'),
sortedChildren: alias('listSorted'),
});
12 changes: 12 additions & 0 deletions ui/app/components/job-page/parts/evaluations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Component from '@ember/component';
import { computed } from '@ember/object';

export default Component.extend({
job: null,

classNames: ['boxed-section'],

sortedEvaluations: computed('job.evaluations.@each.modifyIndex', function() {
return (this.get('job.evaluations') || []).sortBy('modifyIndex').reverse();
}),
});
6 changes: 6 additions & 0 deletions ui/app/components/job-page/parts/placement-failures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from '@ember/component';

export default Component.extend({
job: null,
tagName: '',
});
6 changes: 6 additions & 0 deletions ui/app/components/job-page/parts/running-deployment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from '@ember/component';

export default Component.extend({
job: null,
tagName: '',
});
7 changes: 7 additions & 0 deletions ui/app/components/job-page/parts/summary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Component from '@ember/component';

export default Component.extend({
job: null,

classNames: ['boxed-section'],
});
24 changes: 24 additions & 0 deletions ui/app/components/job-page/parts/task-groups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import Sortable from 'nomad-ui/mixins/sortable';

export default Component.extend(Sortable, {
job: null,

classNames: ['boxed-section'],

// Provide a value that is bound to a query param
sortProperty: null,
sortDescending: null,

// Provide an action with access to the router
gotoTaskGroup() {},

taskGroups: computed('job.taskGroups.[]', function() {
return this.get('job.taskGroups') || [];
}),

listToSort: alias('taskGroups'),
sortedTaskGroups: alias('listSorted'),
});
21 changes: 21 additions & 0 deletions ui/app/components/job-page/periodic-child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import AbstractJobPage from './abstract';
import { computed } from '@ember/object';

export default AbstractJobPage.extend({
breadcrumbs: computed('job.{name,id}', 'job.parent.{name,id}', function() {
const job = this.get('job');
const parent = this.get('job.parent');

return [
{ label: 'Jobs', args: ['jobs'] },
{
label: parent.get('name'),
args: ['jobs.job', parent],
},
{
label: job.get('trimmedName'),
args: ['jobs.job', job],
},
];
}),
});
15 changes: 15 additions & 0 deletions ui/app/components/job-page/periodic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import AbstractJobPage from './abstract';
import { inject as service } from '@ember/service';

export default AbstractJobPage.extend({
store: service(),
actions: {
forceLaunch() {
this.get('job')
.forcePeriodic()
.then(() => {
this.get('store').findAll('job');
});
},
},
});
3 changes: 3 additions & 0 deletions ui/app/components/job-page/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import AbstractJobPage from './abstract';

export default AbstractJobPage.extend();
22 changes: 12 additions & 10 deletions ui/app/controllers/jobs/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inject as service } from '@ember/service';
import { alias, filterBy } from '@ember/object/computed';
import { alias } from '@ember/object/computed';
import Controller, { inject as controller } from '@ember/controller';
import { computed } from '@ember/object';
import Sortable from 'nomad-ui/mixins/sortable';
Expand All @@ -11,10 +11,6 @@ export default Controller.extend(Sortable, Searchable, {

isForbidden: alias('jobsController.isForbidden'),

pendingJobs: filterBy('model', 'status', 'pending'),
runningJobs: filterBy('model', 'status', 'running'),
deadJobs: filterBy('model', 'status', 'dead'),

queryParams: {
currentPage: 'page',
searchTerm: 'search',
Expand All @@ -30,16 +26,22 @@ export default Controller.extend(Sortable, Searchable, {

searchProps: computed(() => ['id', 'name']),

/**
Filtered jobs are those that match the selected namespace and aren't children
of periodic or parameterized jobs.
*/
filteredJobs: computed(
'model.[]',
'model.@each.parent',
'system.activeNamespace',
'system.namespaces.length',
function() {
if (this.get('system.namespaces.length')) {
return this.get('model').filterBy('namespace.id', this.get('system.activeNamespace.id'));
} else {
return this.get('model');
}
const hasNamespaces = this.get('system.namespaces.length');
const activeNamespace = this.get('system.activeNamespace.id');

return this.get('model')
.filter(job => !hasNamespaces || job.get('namespace.id') === activeNamespace)
.filter(job => !job.get('parent.content'));
}
),

Expand Down
22 changes: 7 additions & 15 deletions ui/app/controllers/jobs/job/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import Controller, { inject as controller } from '@ember/controller';
import { computed } from '@ember/object';
import Sortable from 'nomad-ui/mixins/sortable';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';

export default Controller.extend(Sortable, WithNamespaceResetting, {
export default Controller.extend(WithNamespaceResetting, {
system: service(),
jobController: controller('jobs.job'),

Expand All @@ -16,28 +14,22 @@ export default Controller.extend(Sortable, WithNamespaceResetting, {
},

currentPage: 1,
pageSize: 10,

sortProperty: 'name',
sortDescending: false,

breadcrumbs: alias('jobController.breadcrumbs'),
job: alias('model'),

taskGroups: computed('model.taskGroups.[]', function() {
return this.get('model.taskGroups') || [];
}),

listToSort: alias('taskGroups'),
sortedTaskGroups: alias('listSorted'),

sortedEvaluations: computed('model.evaluations.@each.modifyIndex', function() {
return (this.get('model.evaluations') || []).sortBy('modifyIndex').reverse();
}),

actions: {
gotoTaskGroup(taskGroup) {
this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup);
},

gotoJob(job) {
this.transitionToRoute('jobs.job', job, {
queryParams: { jobNamespace: job.get('namespace.name') },
});
},
},
});
Loading