From 633c9599dcb5ad1087539ef86ffc9bb24bf1e5b0 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:23:16 -0800 Subject: [PATCH 01/34] Filter child jobs out of the root jobs list --- ui/app/controllers/jobs/index.js | 22 ++++++++++++---------- ui/app/serializers/job.js | 13 +++++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/ui/app/controllers/jobs/index.js b/ui/app/controllers/jobs/index.js index d0d06e9ee..db1a561c0 100644 --- a/ui/app/controllers/jobs/index.js +++ b/ui/app/controllers/jobs/index.js @@ -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'; @@ -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', @@ -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')); } ), diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index e09c95cd8..7e42a54af 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -15,6 +15,19 @@ export default ApplicationSerializer.extend({ hash.PlainId = hash.ID; hash.ID = JSON.stringify([hash.ID, hash.NamespaceID || 'default']); + // ParentID comes in as "" instead of null + if (!hash.ParentID) { + hash.ParentID = null; + } else { + hash.ParentID = JSON.stringify([hash.ParentID, hash.NamespaceID || 'default']); + } + + // Periodic is a boolean on list and an object on single + if (hash.Periodic instanceof Object) { + hash.PeriodicDetails = hash.Periodic; + hash.Periodic = true; + } + // Transform the map-based JobSummary object into an array-based // JobSummary fragment list hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary') || {}).map(key => { From c3fe28219b6c83b3528cd4036051920ad97db08c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:24:13 -0800 Subject: [PATCH 02/34] Model the parent/child relationship in jobs --- ui/app/models/job.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 511f89856..89ff58108 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -1,4 +1,4 @@ -import { collect, sum, bool, equal } from '@ember/object/computed'; +import { collect, sum, bool, equal, or } from '@ember/object/computed'; import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; @@ -19,9 +19,16 @@ export default Model.extend({ createIndex: attr('number'), modifyIndex: attr('number'), + // True when the job is the parent periodic or parameterized jobs + // Instances of periodic or parameterized jobs are false for both properties periodic: attr('boolean'), parameterized: attr('boolean'), + hasChildren: or('periodic', 'parameterized'), + + parent: belongsTo('job', { inverse: 'children' }), + children: hasMany('job', { inverse: 'parent' }), + datacenters: attr(), taskGroups: fragmentArray('task-group', { defaultValue: () => [] }), taskGroupSummaries: fragmentArray('task-group-summary'), From d8bd9dec773631f366c1da3b92c4c4ddb183f032 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:25:45 -0800 Subject: [PATCH 03/34] Specialized children-status-bar variant of the allocation-status-bar --- ui/app/components/children-status-bar.js | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 ui/app/components/children-status-bar.js diff --git a/ui/app/components/children-status-bar.js b/ui/app/components/children-status-bar.js new file mode 100644 index 000000000..cb32db128 --- /dev/null +++ b/ui/app/components/children-status-bar.js @@ -0,0 +1,25 @@ +import { computed } from '@ember/object'; +import DistributionBar from './distribution-bar'; + +export default DistributionBar.extend({ + layoutName: 'components/distribution-bar', + + job: null, + + 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' }, + ]; + }), +}); From 67cee01f112d33b95f3574f029f5340a2c042088 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:26:07 -0800 Subject: [PATCH 04/34] Use the children summary instead of alloc summary when applicable --- ui/app/templates/components/job-row.hbs | 8 +++++++- ui/app/templates/jobs/index.hbs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index 54032c664..d2c62b494 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -12,5 +12,11 @@ {{/if}} -
{{allocation-status-bar allocationContainer=job isNarrow=true}}
+
+ {{#if job.hasChildren}} + {{children-status-bar job=job isNarrow=true}} + {{else}} + {{allocation-status-bar allocationContainer=job isNarrow=true}} + {{/if}} +
diff --git a/ui/app/templates/jobs/index.hbs b/ui/app/templates/jobs/index.hbs index 9c8c93888..34e24c33a 100644 --- a/ui/app/templates/jobs/index.hbs +++ b/ui/app/templates/jobs/index.hbs @@ -28,7 +28,7 @@ {{#t.sort-by prop="type"}}Type{{/t.sort-by}} {{#t.sort-by prop="priority"}}Priority{{/t.sort-by}} Groups - Allocation Status + Summary {{/t.head}} {{#t.body key="model.id" as |row|}} {{job-row data-test-job-row job=row.model onClick=(action "gotoJob" row.model)}} From 26c76e67f7b60dd0ae7fa77de022026235dd6420 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:34:19 -0800 Subject: [PATCH 05/34] Fix lint-staged paths Paths now start from package.json location, not project root. --- ui/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/package.json b/ui/package.json index 914d94ad2..7866bcc6b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,11 +14,11 @@ "precommit": "lint-staged" }, "lint-staged": { - "ui/{app,tests,config,lib,mirage}/**/*.js": [ + "{app,tests,config,lib,mirage}/**/*.js": [ "prettier --write", "git add" ], - "ui/app/styles/**/*.*": [ + "app/styles/**/*.*": [ "prettier --write", "git add" ] From a4deea08af9b03225b69aa857f2db7f1fb7ded68 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 09:29:47 -0800 Subject: [PATCH 06/34] Computed a template type for a job This is a composite of scheduler type, batch variations, and children v. template --- ui/app/models/job.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 89ff58108..c6ab54835 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -6,6 +6,8 @@ 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']; + export default Model.extend({ region: attr('string'), name: attr('string'), @@ -24,11 +26,44 @@ export default Model.extend({ periodic: attr('boolean'), parameterized: attr('boolean'), + periodicDetails: attr(), + parameterizedDetails: attr(), + hasChildren: or('periodic', 'parameterized'), parent: belongsTo('job', { inverse: 'children' }), children: hasMany('job', { inverse: 'parent' }), + // A composite of type and other job attributes to determine + // type for templating rather than scheduling + templateType: computed( + 'type', + 'periodic', + 'parameterized', + 'parent.periodic', + 'parent.parameterized', + function() { + const type = this.get('type'); + + if (this.get('periodic')) { + return 'periodic'; + } else if (this.get('parameterized')) { + return 'parameterized'; + } else if (this.get('parent.periodic')) { + return 'periodic-child'; + } else if (this.get('parent.parameterized')) { + return 'parameterized-child'; + } else if (JOB_TYPES.includes(type)) { + // Guard against the API introducing a new type before the UI + // is prepared to handle it. + return this.get('type'); + } else { + // A fail-safe in the event the API introduces a new type. + return 'service'; + } + } + ), + datacenters: attr(), taskGroups: fragmentArray('task-group', { defaultValue: () => [] }), taskGroupSummaries: fragmentArray('task-group-summary'), @@ -56,6 +91,12 @@ export default Model.extend({ runningChildren: attr('number'), deadChildren: attr('number'), + childrenList: collect('pendingChildren', 'runningChildren', 'deadChildren'), + + totalChildren: sum('childrenList'), + + version: attr('number'), + versions: hasMany('job-versions'), allocations: hasMany('allocations'), deployments: hasMany('deployments'), From 1845ccd4568c2e1ecc6ebc0f3ca6d1c595e40296 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 09:30:46 -0800 Subject: [PATCH 07/34] Handle the difference between parameterized on single and list responses --- ui/app/serializers/job.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index 7e42a54af..df77e2f38 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -28,6 +28,12 @@ export default ApplicationSerializer.extend({ hash.Periodic = true; } + // Parameterized behaves like Periodic + if (hash.ParameterizedJob instanceof Object) { + hash.ParameterizedDetails = hash.ParameterizedJob; + 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 => { From 25d9004d3a11f0b1344c2b60aad444e6f9973b5e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 09:31:48 -0800 Subject: [PATCH 08/34] Deconstruct the existing job detail page into common parts This is to later compose job detail page variations --- .../components/job-page/parts/evaluations.js | 12 ++++++ .../job-page/parts/placement-failures.js | 6 +++ .../job-page/parts/running-deployment.js | 6 +++ ui/app/components/job-page/parts/summary.js | 7 ++++ .../components/job-page/parts/task-groups.js | 23 +++++++++++ .../components/job-page/parts/body.hbs | 6 +++ .../components/job-page/parts/evaluations.hbs | 38 +++++++++++++++++++ .../job-page/parts/placement-failures.hbs | 12 ++++++ .../job-page/parts/running-deployment.hbs | 33 ++++++++++++++++ .../components/job-page/parts/summary.hbs | 27 +++++++++++++ .../components/job-page/parts/task-groups.hbs | 25 ++++++++++++ 11 files changed, 195 insertions(+) create mode 100644 ui/app/components/job-page/parts/evaluations.js create mode 100644 ui/app/components/job-page/parts/placement-failures.js create mode 100644 ui/app/components/job-page/parts/running-deployment.js create mode 100644 ui/app/components/job-page/parts/summary.js create mode 100644 ui/app/components/job-page/parts/task-groups.js create mode 100644 ui/app/templates/components/job-page/parts/body.hbs create mode 100644 ui/app/templates/components/job-page/parts/evaluations.hbs create mode 100644 ui/app/templates/components/job-page/parts/placement-failures.hbs create mode 100644 ui/app/templates/components/job-page/parts/running-deployment.hbs create mode 100644 ui/app/templates/components/job-page/parts/summary.hbs create mode 100644 ui/app/templates/components/job-page/parts/task-groups.hbs diff --git a/ui/app/components/job-page/parts/evaluations.js b/ui/app/components/job-page/parts/evaluations.js new file mode 100644 index 000000000..33f6054a7 --- /dev/null +++ b/ui/app/components/job-page/parts/evaluations.js @@ -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(); + }), +}); diff --git a/ui/app/components/job-page/parts/placement-failures.js b/ui/app/components/job-page/parts/placement-failures.js new file mode 100644 index 000000000..7df4236d8 --- /dev/null +++ b/ui/app/components/job-page/parts/placement-failures.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; + +export default Component.extend({ + job: null, + tagName: '', +}); diff --git a/ui/app/components/job-page/parts/running-deployment.js b/ui/app/components/job-page/parts/running-deployment.js new file mode 100644 index 000000000..7df4236d8 --- /dev/null +++ b/ui/app/components/job-page/parts/running-deployment.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; + +export default Component.extend({ + job: null, + tagName: '', +}); diff --git a/ui/app/components/job-page/parts/summary.js b/ui/app/components/job-page/parts/summary.js new file mode 100644 index 000000000..0ff44fc5a --- /dev/null +++ b/ui/app/components/job-page/parts/summary.js @@ -0,0 +1,7 @@ +import Component from '@ember/component'; + +export default Component.extend({ + job: null, + + classNames: ['boxed-section'], +}); diff --git a/ui/app/components/job-page/parts/task-groups.js b/ui/app/components/job-page/parts/task-groups.js new file mode 100644 index 000000000..a1829c33b --- /dev/null +++ b/ui/app/components/job-page/parts/task-groups.js @@ -0,0 +1,23 @@ +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, + + gotoTaskGroup() {}, + + taskGroups: computed('job.taskGroups.[]', function() { + return this.get('job.taskGroups') || []; + }), + + listToSort: alias('taskGroups'), + sortedTaskGroups: alias('listSorted'), +}); diff --git a/ui/app/templates/components/job-page/parts/body.hbs b/ui/app/templates/components/job-page/parts/body.hbs new file mode 100644 index 000000000..12c339ce4 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/body.hbs @@ -0,0 +1,6 @@ +{{#gutter-menu class="page-body" onNamespaceChange=onNamespaceChange}} + {{partial "jobs/job/subnav"}} +
+ {{yield}} +
+{{/gutter-menu}} diff --git a/ui/app/templates/components/job-page/parts/evaluations.hbs b/ui/app/templates/components/job-page/parts/evaluations.hbs new file mode 100644 index 000000000..f49a6f10b --- /dev/null +++ b/ui/app/templates/components/job-page/parts/evaluations.hbs @@ -0,0 +1,38 @@ +
+ Evaluations +
+
+ {{#if sortedEvaluations.length}} + {{#list-table source=sortedEvaluations as |t|}} + {{#t.head}} + ID + Priority + Triggered By + Status + Placement Failures + {{/t.head}} + {{#t.body as |row|}} + + {{row.model.shortId}} + {{row.model.priority}} + {{row.model.triggeredBy}} + {{row.model.status}} + + {{#if (eq row.model.status "blocked")}} + N/A - In Progress + {{else if row.model.hasPlacementFailures}} + True + {{else}} + False + {{/if}} + + + {{/t.body}} + {{/list-table}} + {{else}} +
+

No Evaluations

+

This is most likely due to garbage collection.

+
+ {{/if}} +
diff --git a/ui/app/templates/components/job-page/parts/placement-failures.hbs b/ui/app/templates/components/job-page/parts/placement-failures.hbs new file mode 100644 index 000000000..f8bf078d0 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/placement-failures.hbs @@ -0,0 +1,12 @@ +{{#if job.hasPlacementFailures}} +
+
+ Placement Failures +
+
+ {{#each job.taskGroups as |taskGroup|}} + {{placement-failure taskGroup=taskGroup}} + {{/each}} +
+
+{{/if}} diff --git a/ui/app/templates/components/job-page/parts/running-deployment.hbs b/ui/app/templates/components/job-page/parts/running-deployment.hbs new file mode 100644 index 000000000..c42bb866d --- /dev/null +++ b/ui/app/templates/components/job-page/parts/running-deployment.hbs @@ -0,0 +1,33 @@ +{{#if job.runningDeployment}} +
+
+
+ Active Deployment + {{job.runningDeployment.shortId}} + {{#if job.runningDeployment.version.submitTime}} + {{moment-from-now job.runningDeployment.version.submitTime}} + {{/if}} +
+
+ Running + {{#if job.runningDeployment.requiresPromotion}} + Deployment is running but requires promotion + {{/if}} +
+
+
+ {{#job-deployment-details deployment=job.runningDeployment as |d|}} + {{d.metrics}} + {{#if isShowingDeploymentDetails}} + {{d.taskGroups}} + {{d.allocations}} + {{/if}} + {{/job-deployment-details}} +
+ +
+{{/if}} diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs new file mode 100644 index 000000000..f00984304 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -0,0 +1,27 @@ +
+
+ {{#if job.hasChildren}} + Children Status {{job.totalChildren}} + {{else}} + Allocation Status {{job.totalAllocs}} + {{/if}} +
+
+
+ {{#component (if job.hasChildren "children-status-bar" "allocation-status-bar") + allocationContainer=job + job=job + class="split-view" as |chart|}} +
    + {{#each chart.data as |datum index|}} +
  1. + + {{datum.value}} + + {{datum.label}} + +
  2. + {{/each}} +
+ {{/component}} +
diff --git a/ui/app/templates/components/job-page/parts/task-groups.hbs b/ui/app/templates/components/job-page/parts/task-groups.hbs new file mode 100644 index 000000000..d6fbf5942 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/task-groups.hbs @@ -0,0 +1,25 @@ +
+
+ Task Groups +
+
+ {{#list-table + source=sortedTaskGroups + sortProperty=sortProperty + sortDescending=sortDescending as |t|}} + {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="count"}}Count{{/t.sort-by}} + {{#t.sort-by prop="queuedOrStartingAllocs" class="is-3"}}Allocation Status{{/t.sort-by}} + {{#t.sort-by prop="reservedCPU"}}Reserved CPU{{/t.sort-by}} + {{#t.sort-by prop="reservedMemory"}}Reserved Memory{{/t.sort-by}} + {{#t.sort-by prop="reservedEphemeralDisk"}}Reserved Disk{{/t.sort-by}} + {{/t.head}} + {{#t.body as |row|}} + {{task-group-row data-test-task-group + taskGroup=row.model + onClick=(action gotoTaskGroup row.model)}} + {{/t.body}} + {{/list-table}} +
+
From 6492611e39676b3c6f87b860422b9935196cdecd Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 09:32:34 -0800 Subject: [PATCH 09/34] Recreate the service job detail page using job part components --- ui/app/components/job-page/abstract.js | 25 +++++++++++++ ui/app/components/job-page/service.js | 3 ++ .../templates/components/job-page/service.hbs | 37 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 ui/app/components/job-page/abstract.js create mode 100644 ui/app/components/job-page/service.js create mode 100644 ui/app/templates/components/job-page/service.hbs diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js new file mode 100644 index 000000000..ecd0cc2b2 --- /dev/null +++ b/ui/app/components/job-page/abstract.js @@ -0,0 +1,25 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + job: null, + + // Provide a value that is bound to a query param + sortProperty: null, + sortDescending: null, + + // Provide actions that require routing + onNamespaceChange() {}, + gotoTaskGroup() {}, + + breadcrumbs: computed('job.{name,id}', function() { + const job = this.get('job'); + return [ + { label: 'Jobs', args: ['jobs'] }, + { + label: job.get('name'), + args: ['jobs.job', job], + }, + ]; + }), +}); diff --git a/ui/app/components/job-page/service.js b/ui/app/components/job-page/service.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/service.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/templates/components/job-page/service.hbs b/ui/app/templates/components/job-page/service.hbs new file mode 100644 index 000000000..47343c49a --- /dev/null +++ b/ui/app/templates/components/job-page/service.hbs @@ -0,0 +1,37 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/running-deployment job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} +{{/job-page/parts/body}} From 0e173f5da5d68fb1e3ab69be25fe3eca84e13a30 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 14:18:53 -0800 Subject: [PATCH 10/34] Fleshing out job page parts and differences --- ui/app/components/job-page/abstract.js | 1 + ui/app/components/job-page/batch.js | 3 + .../job-page/parameterized-child.js | 3 + ui/app/components/job-page/parameterized.js | 3 + ui/app/components/job-page/parts/children.js | 31 ++++ .../components/job-page/parts/task-groups.js | 1 + ui/app/components/job-page/periodic-child.js | 3 + ui/app/components/job-page/periodic.js | 3 + ui/app/controllers/jobs/job/index.js | 22 +-- ui/app/templates/jobs/job/index.hbs | 170 +----------------- 10 files changed, 63 insertions(+), 177 deletions(-) create mode 100644 ui/app/components/job-page/batch.js create mode 100644 ui/app/components/job-page/parameterized-child.js create mode 100644 ui/app/components/job-page/parameterized.js create mode 100644 ui/app/components/job-page/parts/children.js create mode 100644 ui/app/components/job-page/periodic-child.js create mode 100644 ui/app/components/job-page/periodic.js diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js index ecd0cc2b2..e9ea804a4 100644 --- a/ui/app/components/job-page/abstract.js +++ b/ui/app/components/job-page/abstract.js @@ -11,6 +11,7 @@ export default Component.extend({ // Provide actions that require routing onNamespaceChange() {}, gotoTaskGroup() {}, + gotoJob() {}, breadcrumbs: computed('job.{name,id}', function() { const job = this.get('job'); diff --git a/ui/app/components/job-page/batch.js b/ui/app/components/job-page/batch.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/batch.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/parameterized-child.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/components/job-page/parameterized.js b/ui/app/components/job-page/parameterized.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/parameterized.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/components/job-page/parts/children.js b/ui/app/components/job-page/parts/children.js new file mode 100644 index 000000000..30772cd0a --- /dev/null +++ b/ui/app/components/job-page/parts/children.js @@ -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'), +}); diff --git a/ui/app/components/job-page/parts/task-groups.js b/ui/app/components/job-page/parts/task-groups.js index a1829c33b..f5ce33757 100644 --- a/ui/app/components/job-page/parts/task-groups.js +++ b/ui/app/components/job-page/parts/task-groups.js @@ -12,6 +12,7 @@ export default Component.extend(Sortable, { sortProperty: null, sortDescending: null, + // Provide an action with access to the router gotoTaskGroup() {}, taskGroups: computed('job.taskGroups.[]', function() { diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/periodic-child.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/components/job-page/periodic.js b/ui/app/components/job-page/periodic.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/periodic.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 97b97efb5..c5cb709a9 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -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'), @@ -16,7 +14,6 @@ export default Controller.extend(Sortable, WithNamespaceResetting, { }, currentPage: 1, - pageSize: 10, sortProperty: 'name', sortDescending: false, @@ -24,20 +21,15 @@ export default Controller.extend(Sortable, WithNamespaceResetting, { 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') }, + }); + }, }, }); diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index bc2e6b7ce..7d81851b2 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -1,162 +1,8 @@ -{{#global-header class="page-header"}} - {{#each breadcrumbs as |breadcrumb index|}} - - {{/each}} -{{/global-header}} -{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}} - {{partial "jobs/job/subnav"}} -
-

- {{model.name}} - {{model.status}} - {{#if model.periodic}} - periodic - {{else if model.parameterized}} - parameterized - {{/if}} -

- -
-
- Type: {{model.type}} | - Priority: {{model.priority}} - {{#if (and model.namespace system.shouldShowNamespaces)}} - | Namespace: {{model.namespace.name}} - {{/if}} -
-
- -
-
-
Allocation Status {{taskGroups.length}}
-
-
- {{#allocation-status-bar allocationContainer=model class="split-view" as |chart|}} -
    - {{#each chart.data as |datum index|}} -
  1. - - {{datum.value}} - - {{datum.label}} - -
  2. - {{/each}} -
- {{/allocation-status-bar}} -
-
- - {{#if model.hasPlacementFailures}} -
-
- Placement Failures -
-
- {{#each model.taskGroups as |taskGroup|}} - {{placement-failure taskGroup=taskGroup}} - {{/each}} -
-
- {{/if}} - - {{#if model.runningDeployment}} -
-
-
- Active Deployment - {{model.runningDeployment.shortId}} - {{#if model.runningDeployment.version.submitTime}} - {{moment-from-now model.runningDeployment.version.submitTime}} - {{/if}} -
-
- Running - {{#if model.runningDeployment.requiresPromotion}} - Deployment is running but requires promotion - {{/if}} -
-
-
- {{#job-deployment-details deployment=model.runningDeployment as |d|}} - {{d.metrics}} - {{#if isShowingDeploymentDetails}} - {{d.taskGroups}} - {{d.allocations}} - {{/if}} - {{/job-deployment-details}} -
- -
- {{/if}} - -
-
- Task Groups -
-
- {{#list-pagination - source=sortedTaskGroups - sortProperty=sortProperty - sortDescending=sortDescending as |p|}} - {{#list-table - source=p.list - sortProperty=sortProperty - sortDescending=sortDescending as |t|}} - {{#t.head}} - {{#t.sort-by prop="name"}}Name{{/t.sort-by}} - {{#t.sort-by prop="count"}}Count{{/t.sort-by}} - {{#t.sort-by prop="queuedOrStartingAllocs" class="is-3"}}Allocation Status{{/t.sort-by}} - {{#t.sort-by prop="reservedCPU"}}Reserved CPU{{/t.sort-by}} - {{#t.sort-by prop="reservedMemory"}}Reserved Memory{{/t.sort-by}} - {{#t.sort-by prop="reservedEphemeralDisk"}}Reserved Disk{{/t.sort-by}} - {{/t.head}} - {{#t.body as |row|}} - {{task-group-row data-test-task-group taskGroup=row.model onClick=(action "gotoTaskGroup" row.model)}} - {{/t.body}} - {{/list-table}} - {{/list-pagination}} -
-
- -
-
- Evaluations -
-
- {{#list-table source=sortedEvaluations as |t|}} - {{#t.head}} - ID - Priority - Triggered By - Status - Placement Failures - {{/t.head}} - {{#t.body as |row|}} - - {{row.model.shortId}} - {{row.model.priority}} - {{row.model.triggeredBy}} - {{row.model.status}} - - {{#if (eq row.model.status "blocked")}} - N/A - In Progress - {{else if row.model.hasPlacementFailures}} - True - {{else}} - False - {{/if}} - - - {{/t.body}} - {{/list-table}} -
-
-
-{{/gutter-menu}} +{{component (concat "job-page/" model.templateType) + job=model + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + onNamespaceChange=(action "gotoJobs") + gotoJob=(action "gotoJob") + gotoTaskGroup=(action "gotoTaskGroup")}} From 995d07efc27d0d73974b0e566cf8bf6d34951c3a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 14:19:18 -0800 Subject: [PATCH 11/34] New job page components for parent jobs and batch jobs --- .../templates/components/job-page/batch.hbs | 35 +++++++++++++++++++ .../components/job-page/parameterized.hbs | 32 +++++++++++++++++ .../components/job-page/periodic.hbs | 33 +++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 ui/app/templates/components/job-page/batch.hbs create mode 100644 ui/app/templates/components/job-page/parameterized.hbs create mode 100644 ui/app/templates/components/job-page/periodic.hbs diff --git a/ui/app/templates/components/job-page/batch.hbs b/ui/app/templates/components/job-page/batch.hbs new file mode 100644 index 000000000..74b7f5c28 --- /dev/null +++ b/ui/app/templates/components/job-page/batch.hbs @@ -0,0 +1,35 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} +{{/job-page/parts/body}} diff --git a/ui/app/templates/components/job-page/parameterized.hbs b/ui/app/templates/components/job-page/parameterized.hbs new file mode 100644 index 000000000..a8e3ed47f --- /dev/null +++ b/ui/app/templates/components/job-page/parameterized.hbs @@ -0,0 +1,32 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} + Parameterized +

+ +
+
+ Version: {{job.version}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} +{{/job-page/parts/body}} diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs new file mode 100644 index 000000000..c172bc9c1 --- /dev/null +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -0,0 +1,33 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} + periodic +

+ +
+
+ Version: {{job.version}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} + | Cron: {{job.periodicDetails.Spec}} +
+
+ + {{job-page/parts/summary job=job}} + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} +{{/job-page/parts/body}} From 0d4a245f7cc15a9ecccba93c1b136110780d05e7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 14:19:47 -0800 Subject: [PATCH 12/34] Paginated and sortable table for job launches/children jobs --- .../components/job-page/parts/children.hbs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 ui/app/templates/components/job-page/parts/children.hbs diff --git a/ui/app/templates/components/job-page/parts/children.hbs b/ui/app/templates/components/job-page/parts/children.hbs new file mode 100644 index 000000000..944168f1b --- /dev/null +++ b/ui/app/templates/components/job-page/parts/children.hbs @@ -0,0 +1,42 @@ +
+ Job Launches +
+
+ {{#list-pagination + source=sortedChildren + size=pageSize + page=currentPage as |p|}} + {{#list-table + source=p.list + sortProperty=sortProperty + sortDescending=sortDescending + class="with-foot" as |t|}} + {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="status"}}Status{{/t.sort-by}} + {{#t.sort-by prop="type"}}Type{{/t.sort-by}} + {{#t.sort-by prop="priority"}}Priority{{/t.sort-by}} + Groups + Summary + {{/t.head}} + {{#t.body key="model.id" as |row|}} + {{job-row data-test-job-row job=row.model onClick=(action gotoJob row.model)}} + {{/t.body}} + {{/list-table}} +
+ +
+ {{else}} +
+

No Job Launches

+

No remaining living job launches.

+
+ {{/list-pagination}} +
From 4f8a59a56c667e4573bfb9d13e665643a3bb6df2 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:50:31 -0800 Subject: [PATCH 13/34] Bring payload in from the job api response --- ui/app/models/job.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index c6ab54835..3b8b98d0a 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -148,4 +148,10 @@ export default Model.extend({ return classMap[this.get('status')] || 'is-dark'; }), + + payload: attr('string'), + decodedPayload: computed('payload', function() { + // Lazily decode the base64 encoded payload + return window.atob(this.get('payload') || ''); + }), }); From 89a19fbe380837b7c641c06e83b549355c8eca31 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:55:09 -0800 Subject: [PATCH 14/34] Breadcrumbs for the periodic child job page --- ui/app/components/job-page/periodic-child.js | 20 ++++++++- .../components/job-page/periodic-child.hbs | 41 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 ui/app/templates/components/job-page/periodic-child.hbs diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js index 559b3c8b8..ea19ed4d3 100644 --- a/ui/app/components/job-page/periodic-child.js +++ b/ui/app/components/job-page/periodic-child.js @@ -1,3 +1,21 @@ import AbstractJobPage from './abstract'; +import { computed } from '@ember/object'; -export default AbstractJobPage.extend(); +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('name'), + args: ['jobs.job', job], + }, + ]; + }), +}); diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs new file mode 100644 index 000000000..387f0a585 --- /dev/null +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -0,0 +1,41 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + + Parent: + {{#link-to "jobs.job" job.parent (query-params jobNamespace=job.parent.namespace.name)}} + {{job.parent.name}} + {{/link-to}} + + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} +{{/job-page/parts/body}} From aa1b9674e83d42a2f7b89e056a8667a587882f0d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:56:52 -0800 Subject: [PATCH 15/34] Elastic mode for cli window component --- ui/app/styles/components/cli-window.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/app/styles/components/cli-window.scss b/ui/app/styles/components/cli-window.scss index f8be6ff7a..0f78d1708 100644 --- a/ui/app/styles/components/cli-window.scss +++ b/ui/app/styles/components/cli-window.scss @@ -12,4 +12,8 @@ .is-light { color: $text; } + + &.is-elastic { + height: auto; + } } From a820ea7b13a23d5f41b86422ea466983ffe084ae Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:57:58 -0800 Subject: [PATCH 16/34] Payload details for the parameterized child job detail page --- .../job-page/parameterized-child.js | 17 +++++- .../job-page/parameterized-child.hbs | 52 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 ui/app/templates/components/job-page/parameterized-child.hbs diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js index 559b3c8b8..841c6fa60 100644 --- a/ui/app/components/job-page/parameterized-child.js +++ b/ui/app/components/job-page/parameterized-child.js @@ -1,3 +1,16 @@ -import AbstractJobPage from './abstract'; +import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import PeriodicChildJobPage from './periodic-child'; -export default AbstractJobPage.extend(); +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; + }), +}); diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs new file mode 100644 index 000000000..245395c9f --- /dev/null +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -0,0 +1,52 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + + Parent: + {{#link-to "jobs.job" job.parent (query-params jobNamespace=job.parent.namespace.name)}} + {{job.parent.name}} + {{/link-to}} + + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} + +
+
Payload
+
+ {{#if payloadJSON}} + {{json-viewer json=payloadJSON}} + {{else}} +
{{payload}}
+ {{/if}} +
+
+{{/job-page/parts/body}} From 732c61baf9ab08b972b2ec418935993f402209f7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:59:30 -0800 Subject: [PATCH 17/34] For now, the system job is identical to the service job --- .../templates/components/job-page/system.hbs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 ui/app/templates/components/job-page/system.hbs diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs new file mode 100644 index 000000000..47343c49a --- /dev/null +++ b/ui/app/templates/components/job-page/system.hbs @@ -0,0 +1,37 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/running-deployment job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} +{{/job-page/parts/body}} From abcfda9911129931da3fe46cb4b97b105a623636 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 14:32:11 -0800 Subject: [PATCH 18/34] Add ability to force a periodic job launch --- ui/app/adapters/job.js | 13 +++++++++++++ ui/app/components/job-page/periodic.js | 14 +++++++++++++- ui/app/models/job.js | 4 ++++ ui/app/templates/components/job-page/periodic.hbs | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js index 80398adac..c783c6ff2 100644 --- a/ui/app/adapters/job.js +++ b/ui/app/adapters/job.js @@ -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'); + } + }, }); diff --git a/ui/app/components/job-page/periodic.js b/ui/app/components/job-page/periodic.js index 559b3c8b8..705d95a2f 100644 --- a/ui/app/components/job-page/periodic.js +++ b/ui/app/components/job-page/periodic.js @@ -1,3 +1,15 @@ import AbstractJobPage from './abstract'; +import { inject as service } from '@ember/service'; -export default AbstractJobPage.extend(); +export default AbstractJobPage.extend({ + store: service(), + actions: { + forceLaunch() { + this.get('job') + .forcePeriodic() + .then(() => { + this.get('store').findAll('job'); + }); + }, + }, +}); diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 3b8b98d0a..1f9786145 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -139,6 +139,10 @@ export default Model.extend({ return this.store.adapterFor('job').fetchRawDefinition(this); }, + forcePeriodic() { + return this.store.adapterFor('job').forcePeriodic(this); + }, + statusClass: computed('status', function() { const classMap = { pending: 'is-pending', diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs index c172bc9c1..6273f1375 100644 --- a/ui/app/templates/components/job-page/periodic.hbs +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -10,6 +10,7 @@ {{job.name}} {{job.status}} periodic +
From 09eeb4978605766fa8bb274fd8724d7b2785f6a1 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 14:36:50 -0800 Subject: [PATCH 19/34] Differentiate between no search matches and no allocs on task group page --- ui/app/templates/jobs/job/task-group.hbs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index 90928f830..2934d2c1a 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -100,12 +100,21 @@
{{else}} -
-
-

No Matches

-

No allocations match the term {{searchTerm}}

+ {{#if allocations.length}} +
+
+

No Matches

+

No allocations match the term {{searchTerm}}

+
-
+ {{else}} +
+
+

No Allocations

+

No allocations have been placed.

+
+
+ {{/if}} {{/list-pagination}}
From 64b99276ca834b3d09f20d101513784c5c2cf29f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 14:56:23 -0800 Subject: [PATCH 20/34] Clean up force launch button --- ui/app/styles/core/buttons.scss | 4 ++++ ui/app/templates/components/job-page/periodic.hbs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index 27d44f0e9..9a360f3c9 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -24,6 +24,10 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2); background: transparent; } + &.is-inline { + vertical-align: middle; + } + &.is-compact { padding: 0.25em 0.75em; margin: -0.25em -0.25em -0.25em 0; diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs index 6273f1375..065c8c96e 100644 --- a/ui/app/templates/components/job-page/periodic.hbs +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -10,7 +10,7 @@ {{job.name}} {{job.status}} periodic - +
From 1f5e9998e9268d8ddf73f0a0297874f452b974eb Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 15:01:10 -0800 Subject: [PATCH 21/34] State periodic or parameterized as the job type when applicable --- ui/app/models/job.js | 18 +++++++++++++++--- ui/app/templates/components/job-row.hbs | 2 +- ui/tests/acceptance/jobs-list-test.js | 18 ++++++++---------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 1f9786145..2fbf90626 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -34,6 +34,18 @@ export default Model.extend({ parent: belongsTo('job', { inverse: 'children' }), children: hasMany('job', { inverse: 'parent' }), + // A composite of type and other job attributes to determine + // a better type descriptor for human interpretation rather + // than for scheduling. + displayType: computed('type', 'periodic', 'parameterized', function() { + if (this.get('periodic')) { + return 'periodic'; + } else if (this.get('parameterized')) { + return 'parameterized'; + } + return this.get('type'); + }), + // A composite of type and other job attributes to determine // type for templating rather than scheduling templateType: computed( @@ -57,10 +69,10 @@ export default Model.extend({ // Guard against the API introducing a new type before the UI // is prepared to handle it. return this.get('type'); - } else { - // A fail-safe in the event the API introduces a new type. - return 'service'; } + + // A fail-safe in the event the API introduces a new type. + return 'service'; } ), diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index d2c62b494..f64d05889 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -2,7 +2,7 @@ {{job.status}} -{{job.type}} +{{job.displayType}} {{job.priority}} {{#if job.isReloading}} diff --git a/ui/tests/acceptance/jobs-list-test.js b/ui/tests/acceptance/jobs-list-test.js index d7f870214..d7a30a1d1 100644 --- a/ui/tests/acceptance/jobs-list-test.js +++ b/ui/tests/acceptance/jobs-list-test.js @@ -59,7 +59,7 @@ test('each job row should contain information about the job', function(assert) { job.status, 'Status' ); - assert.equal(jobRow.querySelector('[data-test-job-type]').textContent, job.type, 'Type'); + assert.equal(jobRow.querySelector('[data-test-job-type]').textContent, typeForJob(job), 'Type'); assert.equal( jobRow.querySelector('[data-test-job-priority]').textContent, job.priority, @@ -99,9 +99,7 @@ test('when there are no jobs, there is an empty message', function(assert) { }); }); -test('when there are jobs, but no matches for a search result, there is an empty message', function( - assert -) { +test('when there are jobs, but no matches for a search result, there is an empty message', function(assert) { server.create('job', { name: 'cat 1' }); server.create('job', { name: 'cat 2' }); @@ -117,9 +115,7 @@ test('when there are jobs, but no matches for a search result, there is an empty }); }); -test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', function( - assert -) { +test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', function(assert) { server.createList('namespace', 2); const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id }); const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id }); @@ -144,9 +140,7 @@ test('when the namespace query param is set, only matching jobs are shown and th }); }); -test('when accessing jobs is forbidden, show a message with a link to the tokens page', function( - assert -) { +test('when accessing jobs is forbidden, show a message with a link to the tokens page', function(assert) { server.pretender.get('/v1/jobs', () => [403, {}, null]); visit('/jobs'); @@ -163,3 +157,7 @@ test('when accessing jobs is forbidden, show a message with a link to the tokens assert.equal(currentURL(), '/settings/tokens'); }); }); + +function typeForJob(job) { + return job.periodic ? 'periodic' : job.parameterized ? 'parameterized' : job.type; +} From 40c814952050561e12d93d404d9f041047fe7a43 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 16:27:04 -0800 Subject: [PATCH 22/34] Trim the parent job prefix off the child job names when displaying them --- ui/app/components/job-page/periodic-child.js | 2 +- ui/app/models/job.js | 5 +++++ ui/app/templates/components/job-page/parameterized-child.hbs | 2 +- ui/app/templates/components/job-page/periodic-child.hbs | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js index ea19ed4d3..060627d93 100644 --- a/ui/app/components/job-page/periodic-child.js +++ b/ui/app/components/job-page/periodic-child.js @@ -13,7 +13,7 @@ export default AbstractJobPage.extend({ args: ['jobs.job', parent], }, { - label: job.get('name'), + label: job.get('trimmedName'), args: ['jobs.job', job], }, ]; diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 2fbf90626..b77fc7a66 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -34,6 +34,11 @@ export default Model.extend({ parent: belongsTo('job', { inverse: 'children' }), children: hasMany('job', { inverse: 'parent' }), + // The parent job name is prepended to child launch job names + trimmedName: computed('name', 'parent', function() { + return this.get('parent.content') ? this.get('name').replace(/.+?\//, '') : this.get('name'); + }), + // A composite of type and other job attributes to determine // a better type descriptor for human interpretation rather // than for scheduling. diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 245395c9f..b01ad400f 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -7,7 +7,7 @@ {{/global-header}} {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}

- {{job.name}} + {{job.trimmedName}} {{job.status}}

diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index 387f0a585..e39aed637 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -7,7 +7,7 @@ {{/global-header}} {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}

- {{job.name}} + {{job.trimmedName}} {{job.status}}

From d8aafcfc7c27743b15718060317f58dc9a103cc8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 30 Jan 2018 13:19:24 -0800 Subject: [PATCH 23/34] Update job factory to use traits for specifying job type --- ui/mirage/factories/job-summary.js | 38 ++++++++++------ ui/mirage/factories/job.js | 69 +++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 20 deletions(-) diff --git a/ui/mirage/factories/job-summary.js b/ui/mirage/factories/job-summary.js index c766b8b22..c76d39e9f 100644 --- a/ui/mirage/factories/job-summary.js +++ b/ui/mirage/factories/job-summary.js @@ -1,4 +1,4 @@ -import { Factory, faker } from 'ember-cli-mirage'; +import { Factory, faker, trait } from 'ember-cli-mirage'; export default Factory.extend({ // Hidden property used to compute the Summary hash @@ -6,17 +6,27 @@ export default Factory.extend({ JobID: '', - Summary: function() { - return this.groupNames.reduce((summary, group) => { - summary[group] = { - Queued: faker.random.number(10), - Complete: faker.random.number(10), - Failed: faker.random.number(10), - Running: faker.random.number(10), - Starting: faker.random.number(10), - Lost: faker.random.number(10), - }; - return summary; - }, {}); - }, + withSummary: trait({ + Summary: function() { + return this.groupNames.reduce((summary, group) => { + summary[group] = { + Queued: faker.random.number(10), + Complete: faker.random.number(10), + Failed: faker.random.number(10), + Running: faker.random.number(10), + Starting: faker.random.number(10), + Lost: faker.random.number(10), + }; + return summary; + }, {}); + }, + }), + + withChildren: trait({ + Children: () => ({ + Pending: faker.random.number(10), + Running: faker.random.number(10), + Dead: faker.random.number(10), + }), + }), }); diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index b18d16d71..6a9103b78 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -1,4 +1,4 @@ -import { Factory, faker } from 'ember-cli-mirage'; +import { Factory, faker, trait } from 'ember-cli-mirage'; import { provide, provider, pickOne } from '../utils'; import { DATACENTERS } from '../common'; @@ -22,10 +22,48 @@ export default Factory.extend({ faker.list.random(...DATACENTERS) ), - periodic: () => Math.random() > 0.5, - parameterized() { - return !this.periodic; - }, + childrenCount: () => faker.random.number({ min: 1, max: 5 }), + + periodic: trait({ + type: 'batch', + periodic: true, + // periodic details object + // serializer update for bool vs details object + periodicDetails: () => ({ + Enabled: true, + ProhibitOverlap: true, + Spec: '*/5 * * * * *', + SpecType: 'cron', + TimeZone: 'UTC', + }), + }), + + parameterized: trait({ + type: 'batch', + parameterized: true, + // parameterized details object + // serializer update for bool vs details object + parameterizedDetails: () => ({ + MetaOptional: null, + MetaRequired: null, + Payload: Math.random() > 0.5 ? 'required' : null, + }), + }), + + periodicChild: trait({ + // Periodic children need a parent job, + // It is the Periodic job's responsibility to create + // periodicChild jobs and provide a parent job. + type: 'batch', + }), + + parameterizedChild: trait({ + // Parameterized children need a parent job, + // It is the Parameterized job's responsibility to create + // parameterizedChild jobs and provide a parent job. + type: 'batch', + payload: window.btoa(faker.lorem.sentence()), + }), createIndex: i => i, modifyIndex: () => faker.random.number({ min: 10, max: 2000 }), @@ -70,7 +108,8 @@ export default Factory.extend({ }); } - const jobSummary = server.create('job-summary', { + const hasChildren = job.periodic || job.parameterized; + const jobSummary = server.create('job-summary', hasChildren ? 'withChildren' : 'withSummary', { groupNames: groups.mapBy('name'), job, }); @@ -102,5 +141,23 @@ export default Factory.extend({ modifyIndex: 4000, }); } + + if (job.periodic) { + // Create periodicChild jobs + server.createList('job', job.childrenCount, 'periodicChild', { + parentId: job.id, + namespaceId: job.namespaceId, + namespace: job.namespace, + }); + } + + if (job.parameterized) { + // Create parameterizedChild jobs + server.createList('job', job.childrenCount, 'parameterizedChild', { + parentId: job.id, + namespaceId: job.namespaceId, + namespace: job.namespace, + }); + } }, }); From 3d9ed6e57e9448db606cefca69bf3cccb476544c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 09:23:41 -0800 Subject: [PATCH 24/34] Integration tests for the body job part --- ui/app/templates/components/gutter-menu.hbs | 2 +- ui/app/templates/jobs/job/subnav.hbs | 2 +- .../integration/job-page/parts/body-test.js | 137 ++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 ui/tests/integration/job-page/parts/body-test.js diff --git a/ui/app/templates/components/gutter-menu.hbs b/ui/app/templates/components/gutter-menu.hbs index 055e8e323..716a623fd 100644 --- a/ui/app/templates/components/gutter-menu.hbs +++ b/ui/app/templates/components/gutter-menu.hbs @@ -42,6 +42,6 @@
-
+
{{yield}}
diff --git a/ui/app/templates/jobs/job/subnav.hbs b/ui/app/templates/jobs/job/subnav.hbs index cfbdeb672..e4ef97f80 100644 --- a/ui/app/templates/jobs/job/subnav.hbs +++ b/ui/app/templates/jobs/job/subnav.hbs @@ -1,4 +1,4 @@ -
+
  • {{#link-to "jobs.job.index" job activeClass="is-active"}}Overview{{/link-to}}
  • {{#link-to "jobs.job.definition" job activeClass="is-active"}}Definition{{/link-to}}
  • diff --git a/ui/tests/integration/job-page/parts/body-test.js b/ui/tests/integration/job-page/parts/body-test.js new file mode 100644 index 000000000..042d1048b --- /dev/null +++ b/ui/tests/integration/job-page/parts/body-test.js @@ -0,0 +1,137 @@ +import { run } from '@ember/runloop'; +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { click, find, findAll } from 'ember-native-dom-helpers'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; +import { clickTrigger } from 'ember-power-select/test-support/helpers'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleForComponent('job-page/parts/body', 'Integration | Component | job-page/parts/body', { + integration: true, + beforeEach() { + window.localStorage.clear(); + this.server = startMirage(); + this.server.createList('namespace', 3); + }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, +}); + +test('includes a subnav for the job', function(assert) { + this.set('job', {}); + this.set('onNamespaceChange', () => {}); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + assert.ok(find('[data-test-subnav="job"]'), 'Job subnav is rendered'); + }); +}); + +test('the subnav includes the deployments link when the job is a service', function(assert) { + const store = getOwner(this).lookup('service:store'); + let job; + + run(() => { + job = store.createRecord('job', { + id: 'service-job', + type: 'service', + }); + }); + + this.set('job', job); + this.set('onNamespaceChange', () => {}); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent); + assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); + assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); + assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); + }); +}); + +test('the subnav does not include the deployments link when the job is not a service', function(assert) { + const store = getOwner(this).lookup('service:store'); + let job; + + run(() => { + job = store.createRecord('job', { + id: 'batch-job', + type: 'batch', + }); + }); + + this.set('job', job); + this.set('onNamespaceChange', () => {}); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent); + assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); + assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); + assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); + }); +}); + +test('body yields content to a section after the subnav', function(assert) { + this.set('job', {}); + this.set('onNamespaceChange', () => {}); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + assert.ok( + find('[data-test-page-content] .section > .inner-content'), + 'Content is rendered in a section in a gutter menu' + ); + assert.ok( + find('[data-test-subnav="job"] + .section > .inner-content'), + 'Content is rendered immediately after the subnav' + ); + }); +}); + +test('onNamespaceChange action is called when the namespace changes in the nested gutter menu', function(assert) { + const namespaceSpy = sinon.spy(); + + this.set('job', {}); + this.set('onNamespaceChange', namespaceSpy); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + clickTrigger('[data-test-namespace-switcher]'); + click(findAll('.ember-power-select-option')[1]); + + return wait().then(() => { + assert.ok(namespaceSpy.calledOnce, 'Switching namespaces calls the onNamespaceChange action'); + }); + }); +}); From d932c79b24bf9891fdd57e04b3d5d3329e7772b6 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 14:01:13 -0800 Subject: [PATCH 25/34] Job part children tests --- ui/mirage/factories/job.js | 2 + .../job-page/parts/children-test.js | 202 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 ui/tests/integration/job-page/parts/children-test.js diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index 6a9103b78..fe97f3573 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -148,6 +148,7 @@ export default Factory.extend({ parentId: job.id, namespaceId: job.namespaceId, namespace: job.namespace, + createAllocations: job.createAllocations, }); } @@ -157,6 +158,7 @@ export default Factory.extend({ parentId: job.id, namespaceId: job.namespaceId, namespace: job.namespace, + createAllocations: job.createAllocations, }); } }, diff --git a/ui/tests/integration/job-page/parts/children-test.js b/ui/tests/integration/job-page/parts/children-test.js new file mode 100644 index 000000000..aecc32c7d --- /dev/null +++ b/ui/tests/integration/job-page/parts/children-test.js @@ -0,0 +1,202 @@ +import { getOwner } from '@ember/application'; +import { assign } from '@ember/polyfills'; +import { run } from '@ember/runloop'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import { findAll, find, click } from 'ember-native-dom-helpers'; +import sinon from 'sinon'; +import { test, moduleForComponent } from 'ember-qunit'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleForComponent('job-page/parts/children', 'Integration | Component | job-page/parts/children', { + integration: true, + beforeEach() { + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, +}); + +const props = (job, options = {}) => + assign( + { + job, + sortProperty: 'name', + sortDescending: true, + currentPage: 1, + gotoJob: () => {}, + }, + options + ); + +test('lists each child', function(assert) { + let parent; + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 3, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + parent = this.store.peekAll('job').findBy('plainId', 'parent'); + }); + + this.setProperties(props(parent)); + + this.render(hbs` + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + assert.equal( + findAll('[data-test-job-name]').length, + parent.get('children.length'), + 'A row for each child' + ); + }); + }); +}); + +test('eventually paginates', function(assert) { + let parent; + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 11, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + parent = this.store.peekAll('job').findBy('plainId', 'parent'); + }); + + this.setProperties(props(parent)); + + this.render(hbs` + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + const childrenCount = parent.get('children.length'); + assert.ok(childrenCount > 10, 'Parent has more children than one page size'); + assert.equal(findAll('[data-test-job-name]').length, 10, 'Table length maxes out at 10'); + assert.ok(find('.pagination-next'), 'Next button is rendered'); + + assert.ok( + new RegExp(`1.10.+?${childrenCount}`).test(find('.pagination-numbers').textContent.trim()) + ); + }); + }); +}); + +test('is sorted based on the sortProperty and sortDescending properties', function(assert) { + let parent; + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 3, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + parent = this.store.peekAll('job').findBy('plainId', 'parent'); + }); + + this.setProperties(props(parent)); + + this.render(hbs` + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + const sortedChildren = parent.get('children').sortBy('name'); + const childRows = findAll('[data-test-job-name]'); + + sortedChildren.reverse().forEach((child, index) => { + assert.equal( + childRows[index].textContent.trim(), + child.get('name'), + `Child ${index} is ${child.get('name')}` + ); + }); + + this.set('sortDescending', false); + + sortedChildren.forEach((child, index) => { + assert.equal( + childRows[index].textContent.trim(), + child.get('name'), + `Child ${index} is ${child.get('name')}` + ); + }); + }); + }); +}); + +test('gotoJob is called when a job row is clicked', function(assert) { + let parent; + const gotoJobSpy = sinon.spy(); + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 1, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + parent = this.store.peekAll('job').findBy('plainId', 'parent'); + }); + + this.setProperties( + props(parent, { + gotoJob: gotoJobSpy, + }) + ); + + this.render(hbs` + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + click('tr.job-row'); + assert.ok( + gotoJobSpy.withArgs(parent.get('children.firstObject')).calledOnce, + 'Clicking the job row calls the gotoJob action' + ); + }); + }); +}); From 395448602f4c1296d69fc5b77c4ab8f547c93f17 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 14:01:32 -0800 Subject: [PATCH 26/34] Job part evaluations test --- .../job-page/parts/evaluations-test.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 ui/tests/integration/job-page/parts/evaluations-test.js diff --git a/ui/tests/integration/job-page/parts/evaluations-test.js b/ui/tests/integration/job-page/parts/evaluations-test.js new file mode 100644 index 000000000..982c1d294 --- /dev/null +++ b/ui/tests/integration/job-page/parts/evaluations-test.js @@ -0,0 +1,65 @@ +import { run } from '@ember/runloop'; +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { findAll } from 'ember-native-dom-helpers'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleForComponent( + 'job-page/parts/evaluations', + 'Integration | Component | job-page/parts/evaluations', + { + integration: true, + beforeEach() { + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, + } +); + +test('lists all evaluations for the job', function(assert) { + let job; + + this.server.create('job', { noFailedPlacements: true, createAllocations: false }); + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + job = this.store.peekAll('job').get('firstObject'); + }); + + this.setProperties({ job }); + + this.render(hbs` + {{job-page/parts/evaluations job=job}} + `); + + return wait().then(() => { + const evaluationRows = findAll('[data-test-evaluation]'); + assert.equal( + evaluationRows.length, + job.get('evaluations.length'), + 'All evaluations are listed' + ); + + job + .get('evaluations') + .sortBy('modifyIndex') + .reverse() + .forEach((evaluation, index) => { + assert.equal( + evaluationRows[index].querySelector('[data-test-id]').textContent.trim(), + evaluation.get('shortId'), + `Evaluation ${index} is ${evaluation.get('shortId')}` + ); + }); + }); + }); +}); From 345a9542917073db094160d65412faee8a5e88e8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 15:27:29 -0800 Subject: [PATCH 27/34] Running deployment job page part tests --- ui/tests/helpers/start-app.js | 2 +- .../job-page/parts/running-deployment-test.js | 141 ++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 ui/tests/integration/job-page/parts/running-deployment-test.js diff --git a/ui/tests/helpers/start-app.js b/ui/tests/helpers/start-app.js index 2ff98a176..304c6e377 100644 --- a/ui/tests/helpers/start-app.js +++ b/ui/tests/helpers/start-app.js @@ -2,7 +2,7 @@ import { run } from '@ember/runloop'; import { merge } from '@ember/polyfills'; import Application from '../../app'; import config from '../../config/environment'; -import registerPowerSelectHelpers from '../../tests/helpers/ember-power-select'; +import registerPowerSelectHelpers from 'ember-power-select/test-support/helpers'; registerPowerSelectHelpers(); diff --git a/ui/tests/integration/job-page/parts/running-deployment-test.js b/ui/tests/integration/job-page/parts/running-deployment-test.js new file mode 100644 index 000000000..e6cb09bbd --- /dev/null +++ b/ui/tests/integration/job-page/parts/running-deployment-test.js @@ -0,0 +1,141 @@ +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { click, find } from 'ember-native-dom-helpers'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import moment from 'moment'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent( + 'job-page/parts/running-deployment', + 'Integration | Component | job-page/parts/running-deployment', + { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, + } +); + +test('there is no active deployment section when the job has no active deployment', function(assert) { + this.server.create('job', { + type: 'service', + noActiveDeployment: true, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + this.render(hbs` + {{job-page/parts/running-deployment job=job}}) + `); + + return wait().then(() => { + assert.notOk(find('[data-test-active-deployment]'), 'No active deployment'); + }); + }); +}); + +test('the active deployment section shows up for the currently running deployment', function(assert) { + this.server.create('job', { type: 'service', createAllocations: false, activeDeployment: true }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + this.render(hbs` + {{job-page/parts/running-deployment job=job}} + `); + + return wait().then(() => { + const deployment = this.get('job.runningDeployment'); + const version = deployment.get('version'); + + assert.ok(find('[data-test-active-deployment]'), 'Active deployment'); + assert.equal( + find('[data-test-active-deployment-stat="id"]').textContent.trim(), + deployment.get('shortId'), + 'The active deployment is the most recent running deployment' + ); + + assert.equal( + find('[data-test-active-deployment-stat="submit-time"]').textContent.trim(), + moment(version.get('submitTime')).fromNow(), + 'Time since the job was submitted is in the active deployment header' + ); + + assert.equal( + find('[data-test-deployment-metric="canaries"]').textContent.trim(), + `${deployment.get('placedCanaries')} / ${deployment.get('desiredCanaries')}`, + 'Canaries, both places and desired, are in the metrics' + ); + + assert.equal( + find('[data-test-deployment-metric="placed"]').textContent.trim(), + deployment.get('placedAllocs'), + 'Placed allocs aggregates across task groups' + ); + + assert.equal( + find('[data-test-deployment-metric="desired"]').textContent.trim(), + deployment.get('desiredTotal'), + 'Desired allocs aggregates across task groups' + ); + + assert.equal( + find('[data-test-deployment-metric="healthy"]').textContent.trim(), + deployment.get('healthyAllocs'), + 'Healthy allocs aggregates across task groups' + ); + + assert.equal( + find('[data-test-deployment-metric="unhealthy"]').textContent.trim(), + deployment.get('unhealthyAllocs'), + 'Unhealthy allocs aggregates across task groups' + ); + + assert.equal( + find('[data-test-deployment-notification]').textContent.trim(), + deployment.get('statusDescription'), + 'Status description is in the metrics block' + ); + }); + }); +}); + +test('the active deployment section can be expanded to show task groups and allocations', function(assert) { + this.server.create('node'); + this.server.create('job', { type: 'service', activeDeployment: true }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + this.render(hbs` + {{job-page/parts/running-deployment job=job}} + `); + + return wait().then(() => { + assert.notOk(find('[data-test-deployment-task-groups]'), 'Task groups not found'); + assert.notOk(find('[data-test-deployment-allocations]'), 'Allocations not found'); + + click('[data-test-deployment-toggle-details]'); + + return wait().then(() => { + assert.ok(find('[data-test-deployment-task-groups]'), 'Task groups found'); + assert.ok(find('[data-test-deployment-allocations]'), 'Allocations found'); + }); + }); + }); +}); From 1b98738a289b0a0366406b6c0a0029700ed7bfd0 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 16:12:50 -0800 Subject: [PATCH 28/34] Tests for the placement failures job part --- .../job-page/parts/placement-failures-test.js | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 ui/tests/integration/job-page/parts/placement-failures-test.js diff --git a/ui/tests/integration/job-page/parts/placement-failures-test.js b/ui/tests/integration/job-page/parts/placement-failures-test.js new file mode 100644 index 000000000..d77467540 --- /dev/null +++ b/ui/tests/integration/job-page/parts/placement-failures-test.js @@ -0,0 +1,86 @@ +import { getOwner } from '@ember/application'; +import { run } from '@ember/runloop'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import { findAll, find } from 'ember-native-dom-helpers'; +import { test, moduleForComponent } from 'ember-qunit'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent( + 'job-page/parts/placement-failures', + 'Integration | Component | job-page/parts/placement-failures', + { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + } +); + +test('when the job has placement failures, they are called out', function(assert) { + this.server.create('job', { failedPlacements: true, createAllocations: false }); + this.store.findAll('job').then(jobs => { + jobs.forEach(job => job.reload()); + }); + + return wait().then(() => { + run(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + }); + + this.render(hbs` + {{job-page/parts/placement-failures job=job}}) + `); + + return wait().then(() => { + const failedEvaluation = this.get('job.evaluations') + .filterBy('hasPlacementFailures') + .sortBy('modifyIndex') + .reverse() + .get('firstObject'); + const failedTGAllocs = failedEvaluation.get('failedTGAllocs'); + + assert.ok(find('[data-test-placement-failures]'), 'Placement failures section found'); + + const taskGroupLabels = findAll('[data-test-placement-failure-task-group]').map(title => + title.textContent.trim() + ); + + failedTGAllocs.forEach(alloc => { + const name = alloc.get('name'); + assert.ok( + taskGroupLabels.find(label => label.includes(name)), + `${name} included in placement failures list` + ); + assert.ok( + taskGroupLabels.find(label => label.includes(alloc.get('coalescedFailures') + 1)), + 'The number of unplaced allocs = CoalescedFailures + 1' + ); + }); + }); + }); +}); + +test('when the job has no placement failures, the placement failures section is gone', function(assert) { + this.server.create('job', { noFailedPlacements: true, createAllocations: false }); + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + }); + + this.render(hbs` + {{job-page/parts/placement-failures job=job}}) + `); + + return wait().then(() => { + assert.notOk(find('[data-test-placement-failures]'), 'Placement failures section not found'); + }); + }); +}); From f864fb3f2a8743c9e34904c5215e9f25488c0698 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 16:55:43 -0800 Subject: [PATCH 29/34] Tests for the summary job page part --- ui/app/components/allocation-status-bar.js | 2 + ui/app/components/children-status-bar.js | 2 + .../components/job-page/parts/summary.hbs | 2 +- .../job-page/parts/summary-test.js | 150 ++++++++++++++++++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 ui/tests/integration/job-page/parts/summary-test.js diff --git a/ui/app/components/allocation-status-bar.js b/ui/app/components/allocation-status-bar.js index 7f2d1e2fc..5322ac97e 100644 --- a/ui/app/components/allocation-status-bar.js +++ b/ui/app/components/allocation-status-bar.js @@ -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() { diff --git a/ui/app/components/children-status-bar.js b/ui/app/components/children-status-bar.js index cb32db128..b95b4f240 100644 --- a/ui/app/components/children-status-bar.js +++ b/ui/app/components/children-status-bar.js @@ -6,6 +6,8 @@ export default DistributionBar.extend({ job: null, + 'data-test-children-status-bar': true, + data: computed('job.{pendingChildren,runningChildren,deadChildren}', function() { if (!this.get('job')) { return []; diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index f00984304..57e2dd25e 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -12,7 +12,7 @@ allocationContainer=job job=job class="split-view" as |chart|}} -
      +
        {{#each chart.data as |datum index|}}
      1. diff --git a/ui/tests/integration/job-page/parts/summary-test.js b/ui/tests/integration/job-page/parts/summary-test.js new file mode 100644 index 000000000..d04d2e82e --- /dev/null +++ b/ui/tests/integration/job-page/parts/summary-test.js @@ -0,0 +1,150 @@ +import { getOwner } from '@ember/application'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import { find } from 'ember-native-dom-helpers'; +import { test, moduleForComponent } from 'ember-qunit'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent('job-page/parts/summary', 'Integration | Component | job-page/parts/summary', { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, +}); + +test('jobs with children use the children diagram', function(assert) { + this.server.create('job', 'periodic', { + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + + this.render(hbs` + {{job-page/parts/summary job=job}} + `); + + return wait().then(() => { + assert.ok(find('[data-test-children-status-bar]'), 'Children status bar found'); + assert.notOk(find('[data-test-allocation-status-bar]'), 'Allocation status bar not found'); + }); + }); +}); + +test('jobs without children use the allocations diagram', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + + this.render(hbs` + {{job-page/parts/summary job=job}} + `); + + return wait().then(() => { + assert.ok(find('[data-test-allocation-status-bar]'), 'Allocation status bar found'); + assert.notOk(find('[data-test-children-status-bar]'), 'Children status bar not found'); + }); + }); +}); + +test('the allocations diagram lists all allocation status figures', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + + this.render(hbs` + {{job-page/parts/summary job=job}} + `); + + return wait().then(() => { + assert.equal( + find('[data-test-legend-value="queued"]').textContent, + this.get('job.queuedAllocs'), + `${this.get('job.queuedAllocs')} are queued` + ); + + assert.equal( + find('[data-test-legend-value="starting"]').textContent, + this.get('job.startingAllocs'), + `${this.get('job.startingAllocs')} are starting` + ); + + assert.equal( + find('[data-test-legend-value="running"]').textContent, + this.get('job.runningAllocs'), + `${this.get('job.runningAllocs')} are running` + ); + + assert.equal( + find('[data-test-legend-value="complete"]').textContent, + this.get('job.completeAllocs'), + `${this.get('job.completeAllocs')} are complete` + ); + + assert.equal( + find('[data-test-legend-value="failed"]').textContent, + this.get('job.failedAllocs'), + `${this.get('job.failedAllocs')} are failed` + ); + + assert.equal( + find('[data-test-legend-value="lost"]').textContent, + this.get('job.lostAllocs'), + `${this.get('job.lostAllocs')} are lost` + ); + }); + }); +}); + +test('the children diagram lists all children status figures', function(assert) { + this.server.create('job', 'periodic', { + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + + this.render(hbs` + {{job-page/parts/summary job=job}} + `); + + return wait().then(() => { + assert.equal( + find('[data-test-legend-value="queued"]').textContent, + this.get('job.pendingChildren'), + `${this.get('job.pendingChildren')} are pending` + ); + + assert.equal( + find('[data-test-legend-value="running"]').textContent, + this.get('job.runningChildren'), + `${this.get('job.runningChildren')} are running` + ); + + assert.equal( + find('[data-test-legend-value="complete"]').textContent, + this.get('job.deadChildren'), + `${this.get('job.deadChildren')} are dead` + ); + }); + }); +}); From b87f65abfce59d8730a7ba2ef150b3efda0343a5 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 1 Feb 2018 17:21:45 -0800 Subject: [PATCH 30/34] Tests for the task groups job page part --- .../job-page/parts/task-groups-test.js | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 ui/tests/integration/job-page/parts/task-groups-test.js diff --git a/ui/tests/integration/job-page/parts/task-groups-test.js b/ui/tests/integration/job-page/parts/task-groups-test.js new file mode 100644 index 000000000..260db31c9 --- /dev/null +++ b/ui/tests/integration/job-page/parts/task-groups-test.js @@ -0,0 +1,171 @@ +import { getOwner } from '@ember/application'; +import { assign } from '@ember/polyfills'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import { click, findAll, find } from 'ember-native-dom-helpers'; +import { test, moduleForComponent } from 'ember-qunit'; +import sinon from 'sinon'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent( + 'job-page/parts/task-groups', + 'Integration | Component | job-page/parts/task-groups', + { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + + afterEach() { + this.server.shutdown(); + }, + } +); + +const props = (job, options = {}) => + assign( + { + job, + sortProperty: 'name', + sortDescending: true, + gotoTaskGroup: () => {}, + }, + options + ); + +test('the job detail page should list all task groups', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job').then(jobs => { + jobs.forEach(job => job.reload()); + }); + + return wait().then(() => { + const job = this.store.peekAll('job').get('firstObject'); + this.setProperties(props(job)); + + this.render(hbs` + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + `); + + return wait().then(() => { + assert.equal( + findAll('[data-test-task-group]').length, + job.get('taskGroups.length'), + 'One row per task group' + ); + }); + }); +}); + +test('each row in the task group table should show basic information about the task group', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job').then(jobs => { + jobs.forEach(job => job.reload()); + }); + + return wait().then(() => { + const job = this.store.peekAll('job').get('firstObject'); + const taskGroup = job + .get('taskGroups') + .sortBy('name') + .reverse() + .get('firstObject'); + + this.setProperties(props(job)); + + this.render(hbs` + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + `); + + return wait().then(() => { + const taskGroupRow = find('[data-test-task-group]'); + + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-name]').textContent.trim(), + taskGroup.get('name'), + 'Name' + ); + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-count]').textContent.trim(), + taskGroup.get('count'), + 'Count' + ); + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-cpu]').textContent.trim(), + `${taskGroup.get('reservedCPU')} MHz`, + 'Reserved CPU' + ); + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-mem]').textContent.trim(), + `${taskGroup.get('reservedMemory')} MiB`, + 'Reserved Memory' + ); + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-disk]').textContent.trim(), + `${taskGroup.get('reservedEphemeralDisk')} MiB`, + 'Reserved Disk' + ); + }); + }); +}); + +test('gotoTaskGroup is called when task group rows are clicked', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job').then(jobs => { + jobs.forEach(job => job.reload()); + }); + + return wait().then(() => { + const taskGroupSpy = sinon.spy(); + const job = this.store.peekAll('job').get('firstObject'); + const taskGroup = job + .get('taskGroups') + .sortBy('name') + .reverse() + .get('firstObject'); + + this.setProperties( + props(job, { + gotoTaskGroup: taskGroupSpy, + }) + ); + + this.render(hbs` + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + `); + + return wait().then(() => { + click('[data-test-task-group]'); + assert.ok( + taskGroupSpy.withArgs(taskGroup).calledOnce, + 'Clicking the task group row calls the gotoTaskGroup action' + ); + }); + }); +}); From d3ea4557a3a5ed3015552206718f3f0790d3642c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 1 Feb 2018 17:22:09 -0800 Subject: [PATCH 31/34] New module-for-job for acceptance testing job detail differences --- ui/tests/acceptance/job-detail-test.js | 356 +++---------------------- ui/tests/helpers/module-for-job.js | 45 ++++ 2 files changed, 75 insertions(+), 326 deletions(-) create mode 100644 ui/tests/helpers/module-for-job.js diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index a1df252ec..ee4377f25 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -1,26 +1,38 @@ -import { get } from '@ember/object'; import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers'; -import moment from 'moment'; -import { test } from 'qunit'; +import { skip } from 'qunit'; import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; +import moduleForJob from 'nomad-ui/tests/helpers/module-for-job'; -const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); +moduleForJob('Acceptance | job detail (batch)', () => server.create('job', { type: 'batch' })); +moduleForJob('Acceptance | job detail (system)', () => server.create('job', { type: 'system' })); +moduleForJob('Acceptance | job detail (periodic)', () => server.create('job', 'periodic')); -let job; +moduleForJob('Acceptance | job detail (parameterized)', () => + server.create('job', 'parameterized') +); -moduleForAcceptance('Acceptance | job detail', { - beforeEach() { - server.create('node'); - job = server.create('job', { type: 'service' }); - visit(`/jobs/${job.id}`); +moduleForJob('Acceptance | job detail (periodic child)', () => { + const parent = server.create('job', 'periodic'); + return server.db.jobs.where({ parentId: parent.id })[0]; +}); + +moduleForJob('Acceptance | job detail (parameterized child)', () => { + const parent = server.create('job', 'parameterized'); + return server.db.jobs.where({ parentId: parent.id })[0]; +}); + +moduleForJob('Acceptance | job detail (service)', () => server.create('job', { type: 'service' }), { + 'the subnav links to deployment': (job, assert) => { + click(find('[data-test-tab="deployments"] a')); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}/deployments`); + }); }, }); -test('visiting /jobs/:job_id', function(assert) { - assert.equal(currentURL(), `/jobs/${job.id}`); -}); +let job; -test('breadcrumbs includes job name and link back to the jobs list', function(assert) { +skip('breadcrumbs includes job name and link back to the jobs list', function(assert) { assert.equal( find('[data-test-breadcrumb="Jobs"]').textContent, 'Jobs', @@ -38,322 +50,14 @@ test('breadcrumbs includes job name and link back to the jobs list', function(as }); }); -test('the subnav includes links to definition, versions, and deployments when type = service', function( - assert -) { - const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent); - assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); - assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); - assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); -}); - -test('the subnav includes links to definition and versions when type != service', function(assert) { - job = server.create('job', { type: 'batch' }); - visit(`/jobs/${job.id}`); - - andThen(() => { - const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent); - assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); - assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); - assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); - }); -}); - -test('the job detail page should contain basic information about the job', function(assert) { +skip('the job detail page should contain basic information about the job', function(assert) { assert.ok(find('[data-test-job-status]').textContent.includes(job.status), 'Status'); assert.ok(find('[data-test-job-stat="type"]').textContent.includes(job.type), 'Type'); assert.ok(find('[data-test-job-stat="priority"]').textContent.includes(job.priority), 'Priority'); assert.notOk(find('[data-test-job-stat="namespace"]'), 'Namespace is not included'); }); -test('the job detail page should list all task groups', function(assert) { - assert.equal( - findAll('[data-test-task-group]').length, - server.db.taskGroups.where({ jobId: job.id }).length - ); -}); - -test('each row in the task group table should show basic information about the task group', function( - assert -) { - const taskGroup = job.taskGroupIds.map(id => server.db.taskGroups.find(id)).sortBy('name')[0]; - const taskGroupRow = find('[data-test-task-group]'); - const tasks = server.db.tasks.where({ taskGroupId: taskGroup.id }); - const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); - - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-name]').textContent.trim(), - taskGroup.name, - 'Name' - ); - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-count]').textContent.trim(), - taskGroup.count, - 'Count' - ); - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-cpu]').textContent.trim(), - `${sum(tasks, 'Resources.CPU')} MHz`, - 'Reserved CPU' - ); - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-mem]').textContent.trim(), - `${sum(tasks, 'Resources.MemoryMB')} MiB`, - 'Reserved Memory' - ); - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-disk]').textContent.trim(), - `${taskGroup.ephemeralDisk.SizeMB} MiB`, - 'Reserved Disk' - ); -}); - -test('the allocations diagram lists all allocation status figures', function(assert) { - const jobSummary = server.db.jobSummaries.findBy({ jobId: job.id }); - const statusCounts = Object.keys(jobSummary.Summary).reduce( - (counts, key) => { - const group = jobSummary.Summary[key]; - counts.queued += group.Queued; - counts.starting += group.Starting; - counts.running += group.Running; - counts.complete += group.Complete; - counts.failed += group.Failed; - counts.lost += group.Lost; - return counts; - }, - { queued: 0, starting: 0, running: 0, complete: 0, failed: 0, lost: 0 } - ); - - assert.equal( - find('[data-test-legend-value="queued"]').textContent, - statusCounts.queued, - `${statusCounts.queued} are queued` - ); - - assert.equal( - find('[data-test-legend-value="starting"]').textContent, - statusCounts.starting, - `${statusCounts.starting} are starting` - ); - - assert.equal( - find('[data-test-legend-value="running"]').textContent, - statusCounts.running, - `${statusCounts.running} are running` - ); - - assert.equal( - find('[data-test-legend-value="complete"]').textContent, - statusCounts.complete, - `${statusCounts.complete} are complete` - ); - - assert.equal( - find('[data-test-legend-value="failed"]').textContent, - statusCounts.failed, - `${statusCounts.failed} are failed` - ); - - assert.equal( - find('[data-test-legend-value="lost"]').textContent, - statusCounts.lost, - `${statusCounts.lost} are lost` - ); -}); - -test('there is no active deployment section when the job has no active deployment', function( - assert -) { - // TODO: it would be better to not visit two different job pages in one test, but this - // way is much more convenient. - job = server.create('job', { noActiveDeployment: true, type: 'service' }); - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.notOk(find('[data-test-active-deployment]'), 'No active deployment'); - }); -}); - -test('the active deployment section shows up for the currently running deployment', function( - assert -) { - job = server.create('job', { activeDeployment: true, type: 'service' }); - const deployment = server.db.deployments.where({ jobId: job.id })[0]; - const taskGroupSummaries = server.db.deploymentTaskGroupSummaries.where({ - deploymentId: deployment.id, - }); - const version = server.db.jobVersions.findBy({ - jobId: job.id, - version: deployment.versionNumber, - }); - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.ok(find('[data-test-active-deployment]'), 'Active deployment'); - assert.equal( - find('[data-test-active-deployment-stat="id"]').textContent.trim(), - deployment.id.split('-')[0], - 'The active deployment is the most recent running deployment' - ); - - assert.equal( - find('[data-test-active-deployment-stat="submit-time"]').textContent.trim(), - moment(version.submitTime / 1000000).fromNow(), - 'Time since the job was submitted is in the active deployment header' - ); - - assert.equal( - find('[data-test-deployment-metric="canaries"]').textContent.trim(), - `${sum(taskGroupSummaries, 'placedCanaries')} / ${sum( - taskGroupSummaries, - 'desiredCanaries' - )}`, - 'Canaries, both places and desired, are in the metrics' - ); - - assert.equal( - find('[data-test-deployment-metric="placed"]').textContent.trim(), - sum(taskGroupSummaries, 'placedAllocs'), - 'Placed allocs aggregates across task groups' - ); - - assert.equal( - find('[data-test-deployment-metric="desired"]').textContent.trim(), - sum(taskGroupSummaries, 'desiredTotal'), - 'Desired allocs aggregates across task groups' - ); - - assert.equal( - find('[data-test-deployment-metric="healthy"]').textContent.trim(), - sum(taskGroupSummaries, 'healthyAllocs'), - 'Healthy allocs aggregates across task groups' - ); - - assert.equal( - find('[data-test-deployment-metric="unhealthy"]').textContent.trim(), - sum(taskGroupSummaries, 'unhealthyAllocs'), - 'Unhealthy allocs aggregates across task groups' - ); - - assert.equal( - find('[data-test-deployment-notification]').textContent.trim(), - deployment.statusDescription, - 'Status description is in the metrics block' - ); - }); -}); - -test('the active deployment section can be expanded to show task groups and allocations', function( - assert -) { - job = server.create('job', { activeDeployment: true, type: 'service' }); - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.notOk(find('[data-test-deployment-task-groups]'), 'Task groups not found'); - assert.notOk(find('[data-test-deployment-allocations]'), 'Allocations not found'); - }); - - andThen(() => { - click('[data-test-deployment-toggle-details]'); - }); - - andThen(() => { - assert.ok(find('[data-test-deployment-task-groups]'), 'Task groups found'); - assert.ok(find('[data-test-deployment-allocations]'), 'Allocations found'); - }); -}); - -test('the evaluations table lists evaluations sorted by modify index', function(assert) { - job = server.create('job'); - const evaluations = server.db.evaluations - .where({ jobId: job.id }) - .sortBy('modifyIndex') - .reverse(); - - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.equal( - findAll('[data-test-evaluation]').length, - evaluations.length, - 'A row for each evaluation' - ); - - evaluations.forEach((evaluation, index) => { - const row = findAll('[data-test-evaluation]')[index]; - assert.equal( - row.querySelector('[data-test-id]').textContent, - evaluation.id.split('-')[0], - `Short ID, row ${index}` - ); - }); - - const firstEvaluation = evaluations[0]; - const row = find('[data-test-evaluation]'); - assert.equal( - row.querySelector('[data-test-priority]').textContent, - '' + firstEvaluation.priority, - 'Priority' - ); - assert.equal( - row.querySelector('[data-test-triggered-by]').textContent, - firstEvaluation.triggeredBy, - 'Triggered By' - ); - assert.equal( - row.querySelector('[data-test-status]').textContent, - firstEvaluation.status, - 'Status' - ); - }); -}); - -test('when the job has placement failures, they are called out', function(assert) { - job = server.create('job', { failedPlacements: true }); - const failedEvaluation = server.db.evaluations - .where({ jobId: job.id }) - .filter(evaluation => evaluation.failedTGAllocs) - .sortBy('modifyIndex') - .reverse()[0]; - - const failedTaskGroupNames = Object.keys(failedEvaluation.failedTGAllocs); - - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.ok(find('[data-test-placement-failures]'), 'Placement failures section found'); - - const taskGroupLabels = findAll('[data-test-placement-failure-task-group]').map(title => - title.textContent.trim() - ); - failedTaskGroupNames.forEach(name => { - assert.ok( - taskGroupLabels.find(label => label.includes(name)), - `${name} included in placement failures list` - ); - assert.ok( - taskGroupLabels.find(label => - label.includes(failedEvaluation.failedTGAllocs[name].CoalescedFailures + 1) - ), - 'The number of unplaced allocs = CoalescedFailures + 1' - ); - }); - }); -}); - -test('when the job has no placement failures, the placement failures section is gone', function( - assert -) { - job = server.create('job', { noFailedPlacements: true }); - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.notOk(find('[data-test-placement-failures]'), 'Placement failures section not found'); - }); -}); - -test('when the job is not found, an error message is shown, but the URL persists', function( +skip('when the job is not found, an error message is shown, but the URL persists', function( assert ) { visit('/jobs/not-a-real-job'); @@ -383,7 +87,7 @@ moduleForAcceptance('Acceptance | job detail (with namespaces)', { }, }); -test('when there are namespaces, the job detail page states the namespace for the job', function( +skip('when there are namespaces, the job detail page states the namespace for the job', function( assert ) { const namespace = server.db.namespaces.find(job.namespaceId); @@ -397,7 +101,7 @@ test('when there are namespaces, the job detail page states the namespace for th }); }); -test('when switching namespaces, the app redirects to /jobs with the new namespace', function( +skip('when switching namespaces, the app redirects to /jobs with the new namespace', function( assert ) { const namespace = server.db.namespaces.find(job.namespaceId); diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js new file mode 100644 index 000000000..1e1c27fa7 --- /dev/null +++ b/ui/tests/helpers/module-for-job.js @@ -0,0 +1,45 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +export default function moduleForJob(title, jobFactory, additionalTests) { + let job; + + moduleForAcceptance(title, { + beforeEach() { + server.create('node'); + job = jobFactory(); + visit(`/jobs/${job.id}`); + }, + }); + + test('visiting /jobs/:job_id', function(assert) { + assert.equal(currentURL(), `/jobs/${job.id}`); + }); + + test('the subnav links to overview', function(assert) { + click(find('[data-test-tab="overview"] a')); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}`); + }); + }); + + test('the subnav links to definition', function(assert) { + click(find('[data-test-tab="definition"] a')); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}/definition`); + }); + }); + + test('the subnav links to versions', function(assert) { + click(find('[data-test-tab="versions"] a')); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}/versions`); + }); + }); + + for (var testName in additionalTests) { + test(testName, function(assert) { + additionalTests[testName](job, assert); + }); + } +} From 9f9597c50c335ea316b09a798f0ea8ffbfcf6339 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 1 Feb 2018 18:54:23 -0800 Subject: [PATCH 32/34] Integration test for periodic job force launch --- .../components/job-page/periodic.hbs | 2 +- ui/mirage/config.js | 17 ++++ .../integration/job-page/periodic-test.js | 89 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 ui/tests/integration/job-page/periodic-test.js diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs index 065c8c96e..6a815b332 100644 --- a/ui/app/templates/components/job-page/periodic.hbs +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -10,7 +10,7 @@ {{job.name}} {{job.status}} periodic - +
        diff --git a/ui/mirage/config.js b/ui/mirage/config.js index b32c70b7c..c17c7d3e5 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -11,6 +11,7 @@ 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'; @@ -58,6 +59,22 @@ export default function() { return this.serialize(deployments.where({ jobId: params.id })); }); + this.post('/job/:id/periodic/force', function(schema, { params }) { + // Create the child job + const parent = schema.jobs.find(params.id); + + // Use the server instead of the schema to leverage the job factory + server.create('job', 'periodicChild', { + parentId: parent.id, + namespaceId: parent.namespaceId, + namespace: parent.namespace, + createAllocations: parent.createAllocations, + }); + + // Return bogus, since the response is normally just eval information + return new Response(200, {}, '{}'); + }); + this.get('/deployment/:id'); this.get('/job/:id/evaluations', function({ evaluations }, { params }) { diff --git a/ui/tests/integration/job-page/periodic-test.js b/ui/tests/integration/job-page/periodic-test.js new file mode 100644 index 000000000..6c6c379a5 --- /dev/null +++ b/ui/tests/integration/job-page/periodic-test.js @@ -0,0 +1,89 @@ +import { run } from '@ember/runloop'; +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { click, find, findAll } from 'ember-native-dom-helpers'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; +import { clickTrigger } from 'ember-power-select/test-support/helpers'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleForComponent('job-page/periodic', 'Integration | Component | job-page/periodic', { + integration: true, + beforeEach() { + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, +}); + +test('Clicking Force Launch launches a new periodic child job', function(assert) { + const childrenCount = 3; + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + const job = this.store.peekAll('job').findBy('plainId', 'parent'); + this.setProperties({ + job, + sortProperty: 'name', + sortDescending: true, + currentPage: 1, + gotoJob: () => {}, + }); + + this.render(hbs` + {{job-page/periodic + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + const currentJobCount = server.db.jobs.length; + + assert.equal( + findAll('[data-test-job-name]').length, + childrenCount, + 'The new periodic job launch is in the children list' + ); + + click('[data-test-force-launch]'); + + return wait().then(() => { + const id = job.get('plainId'); + const namespace = job.get('namespace.name') || 'default'; + + assert.ok( + server.pretender.handledRequests + .filterBy('method', 'POST') + .find(req => req.url === `/v1/job/${id}/periodic/force?namespace=${namespace}`), + 'POST URL was correct' + ); + + assert.ok(server.db.jobs.length, currentJobCount + 1, 'POST request was made'); + + return wait().then(() => { + assert.equal( + findAll('[data-test-job-name]').length, + childrenCount + 1, + 'The new periodic job launch is in the children list' + ); + }); + }); + }); + }); +}); From 2563e04a6d958931893662700be45646c29eea33 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 2 Feb 2018 09:49:18 -0800 Subject: [PATCH 33/34] Inject system so namespace shows up on job detail components --- ui/app/components/job-page/abstract.js | 3 ++ ui/tests/acceptance/job-detail-test.js | 41 +++---------------- .../integration/job-page/periodic-test.js | 5 +-- 3 files changed, 9 insertions(+), 40 deletions(-) diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js index e9ea804a4..eb80479d7 100644 --- a/ui/app/components/job-page/abstract.js +++ b/ui/app/components/job-page/abstract.js @@ -1,7 +1,10 @@ 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 diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index ee4377f25..be2593488 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -1,5 +1,5 @@ import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers'; -import { skip } from 'qunit'; +import { test } from 'qunit'; import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; import moduleForJob from 'nomad-ui/tests/helpers/module-for-job'; @@ -32,34 +32,7 @@ moduleForJob('Acceptance | job detail (service)', () => server.create('job', { t let job; -skip('breadcrumbs includes job name and link back to the jobs list', function(assert) { - assert.equal( - find('[data-test-breadcrumb="Jobs"]').textContent, - 'Jobs', - 'First breadcrumb says jobs' - ); - assert.equal( - find(`[data-test-breadcrumb="${job.name}"]`).textContent, - job.name, - 'Second breadcrumb says the job name' - ); - - click(find('[data-test-breadcrumb="Jobs"]')); - andThen(() => { - assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs'); - }); -}); - -skip('the job detail page should contain basic information about the job', function(assert) { - assert.ok(find('[data-test-job-status]').textContent.includes(job.status), 'Status'); - assert.ok(find('[data-test-job-stat="type"]').textContent.includes(job.type), 'Type'); - assert.ok(find('[data-test-job-stat="priority"]').textContent.includes(job.priority), 'Priority'); - assert.notOk(find('[data-test-job-stat="namespace"]'), 'Namespace is not included'); -}); - -skip('when the job is not found, an error message is shown, but the URL persists', function( - assert -) { +test('when the job is not found, an error message is shown, but the URL persists', function(assert) { visit('/jobs/not-a-real-job'); andThen(() => { @@ -82,14 +55,12 @@ moduleForAcceptance('Acceptance | job detail (with namespaces)', { beforeEach() { server.createList('namespace', 2); server.create('node'); - job = server.create('job', { namespaceId: server.db.namespaces[1].name }); + job = server.create('job', { type: 'service', namespaceId: server.db.namespaces[1].name }); server.createList('job', 3, { namespaceId: server.db.namespaces[0].name }); }, }); -skip('when there are namespaces, the job detail page states the namespace for the job', function( - assert -) { +test('when there are namespaces, the job detail page states the namespace for the job', function(assert) { const namespace = server.db.namespaces.find(job.namespaceId); visit(`/jobs/${job.id}?namespace=${namespace.name}`); @@ -101,9 +72,7 @@ skip('when there are namespaces, the job detail page states the namespace for th }); }); -skip('when switching namespaces, the app redirects to /jobs with the new namespace', function( - assert -) { +test('when switching namespaces, the app redirects to /jobs with the new namespace', function(assert) { const namespace = server.db.namespaces.find(job.namespaceId); const otherNamespace = server.db.namespaces.toArray().find(ns => ns !== namespace).name; const label = otherNamespace === 'default' ? 'Default Namespace' : otherNamespace; diff --git a/ui/tests/integration/job-page/periodic-test.js b/ui/tests/integration/job-page/periodic-test.js index 6c6c379a5..c259f0167 100644 --- a/ui/tests/integration/job-page/periodic-test.js +++ b/ui/tests/integration/job-page/periodic-test.js @@ -1,11 +1,8 @@ -import { run } from '@ember/runloop'; import { getOwner } from '@ember/application'; import { test, moduleForComponent } from 'ember-qunit'; -import { click, find, findAll } from 'ember-native-dom-helpers'; +import { click, findAll } from 'ember-native-dom-helpers'; import wait from 'ember-test-helpers/wait'; import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import { clickTrigger } from 'ember-power-select/test-support/helpers'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; moduleForComponent('job-page/periodic', 'Integration | Component | job-page/periodic', { From 6d46d81bd9fc43992decf337d116de30fc1e911e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 2 Feb 2018 09:51:44 -0800 Subject: [PATCH 34/34] Always shutdown the mirage server --- ui/tests/integration/job-page/parts/children-test.js | 4 ++++ .../integration/job-page/parts/placement-failures-test.js | 4 ++++ ui/tests/integration/job-page/parts/summary-test.js | 4 ++++ ui/tests/integration/job-page/parts/task-groups-test.js | 1 - 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ui/tests/integration/job-page/parts/children-test.js b/ui/tests/integration/job-page/parts/children-test.js index aecc32c7d..9b7149d41 100644 --- a/ui/tests/integration/job-page/parts/children-test.js +++ b/ui/tests/integration/job-page/parts/children-test.js @@ -16,6 +16,10 @@ moduleForComponent('job-page/parts/children', 'Integration | Component | job-pag this.server = startMirage(); this.server.create('namespace'); }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, }); const props = (job, options = {}) => diff --git a/ui/tests/integration/job-page/parts/placement-failures-test.js b/ui/tests/integration/job-page/parts/placement-failures-test.js index d77467540..844e51c5b 100644 --- a/ui/tests/integration/job-page/parts/placement-failures-test.js +++ b/ui/tests/integration/job-page/parts/placement-failures-test.js @@ -19,6 +19,10 @@ moduleForComponent( this.server = startMirage(); this.server.create('namespace'); }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, } ); diff --git a/ui/tests/integration/job-page/parts/summary-test.js b/ui/tests/integration/job-page/parts/summary-test.js index d04d2e82e..186c4ffcf 100644 --- a/ui/tests/integration/job-page/parts/summary-test.js +++ b/ui/tests/integration/job-page/parts/summary-test.js @@ -15,6 +15,10 @@ moduleForComponent('job-page/parts/summary', 'Integration | Component | job-page this.server = startMirage(); this.server.create('namespace'); }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, }); test('jobs with children use the children diagram', function(assert) { diff --git a/ui/tests/integration/job-page/parts/task-groups-test.js b/ui/tests/integration/job-page/parts/task-groups-test.js index 260db31c9..f9ac811f8 100644 --- a/ui/tests/integration/job-page/parts/task-groups-test.js +++ b/ui/tests/integration/job-page/parts/task-groups-test.js @@ -20,7 +20,6 @@ moduleForComponent( this.server = startMirage(); this.server.create('namespace'); }, - afterEach() { this.server.shutdown(); },