From 2f5cbbffa3a4de69df254962e670429270ed52b8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 23 Jul 2020 21:39:50 -0700 Subject: [PATCH 01/15] Data modeling for job scale and scale events --- ui/app/adapters/job-scale.js | 3 +++ ui/app/models/job-scale.js | 11 +++++++++++ ui/app/models/job.js | 1 + ui/app/models/scale-event.js | 18 ++++++++++++++++++ ui/app/models/task-group-scale.js | 17 +++++++++++++++++ ui/app/models/task-group.js | 5 +++++ ui/app/serializers/job-scale.js | 19 +++++++++++++++++++ ui/app/serializers/job.js | 5 +++++ ui/app/serializers/scale-event.js | 10 ++++++++++ 9 files changed, 89 insertions(+) create mode 100644 ui/app/adapters/job-scale.js create mode 100644 ui/app/models/job-scale.js create mode 100644 ui/app/models/scale-event.js create mode 100644 ui/app/models/task-group-scale.js create mode 100644 ui/app/serializers/job-scale.js create mode 100644 ui/app/serializers/scale-event.js diff --git a/ui/app/adapters/job-scale.js b/ui/app/adapters/job-scale.js new file mode 100644 index 00000000000..8cca86df718 --- /dev/null +++ b/ui/app/adapters/job-scale.js @@ -0,0 +1,3 @@ +import WatchableNamespaceIDs from './watchable-namespace-ids'; + +export default class JobScaleAdapter extends WatchableNamespaceIDs {} diff --git a/ui/app/models/job-scale.js b/ui/app/models/job-scale.js new file mode 100644 index 00000000000..54424d9b6ec --- /dev/null +++ b/ui/app/models/job-scale.js @@ -0,0 +1,11 @@ +import Model from 'ember-data/model'; +import { belongsTo } from 'ember-data/relationships'; +import { fragmentArray } from 'ember-data-model-fragments/attributes'; +import classic from 'ember-classic-decorator'; + +@classic +export default class JobSummary extends Model { + @belongsTo('job') job; + + @fragmentArray('task-group-scale') taskGroupScales; +} diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 154fc07b114..86717ea0af9 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -119,6 +119,7 @@ export default class Job extends Model { @hasMany('deployments') deployments; @hasMany('evaluations') evaluations; @belongsTo('namespace') namespace; + @belongsTo('job-scale') scaleState; @computed('taskGroups.@each.drivers') get drivers() { diff --git a/ui/app/models/scale-event.js b/ui/app/models/scale-event.js new file mode 100644 index 00000000000..ecab7bc4643 --- /dev/null +++ b/ui/app/models/scale-event.js @@ -0,0 +1,18 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; + +export default class ScaleEvent extends Fragment { + @fragmentOwner() taskGroupScale; + + @attr('number') count; + @attr('number') previousCount; + @attr('boolean') error; + @attr('string') evalId; + + @attr('date') time; + @attr('number') timeNanos; + + @attr('string') message; + @attr() meta; +} diff --git a/ui/app/models/task-group-scale.js b/ui/app/models/task-group-scale.js new file mode 100644 index 00000000000..83fa05d2efe --- /dev/null +++ b/ui/app/models/task-group-scale.js @@ -0,0 +1,17 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; + +export default class TaskGroupScale extends Fragment { + @fragmentOwner() jobScale; + + @attr('string') name; + + @attr('number') desired; + @attr('number') placed; + @attr('number') running; + @attr('number') healthy; + @attr('number') unhealthy; + + @fragmentArray('scale-event') events; +} diff --git a/ui/app/models/task-group.js b/ui/app/models/task-group.js index 4926f0affa7..e92c2c82c94 100644 --- a/ui/app/models/task-group.js +++ b/ui/app/models/task-group.js @@ -54,6 +54,11 @@ export default class TaskGroup extends Fragment { return maybe(this.get('job.taskGroupSummaries')).findBy('name', this.name); } + @computed('job.scaleState.taskGroupScales.[]') + get scaleState() { + return maybe(this.get('job.scaleState.taskGroupScales')).findBy('name', this.name); + } + scale(count, reason) { return this.job.scale(this.name, count, reason); } diff --git a/ui/app/serializers/job-scale.js b/ui/app/serializers/job-scale.js new file mode 100644 index 00000000000..b0ab04d874a --- /dev/null +++ b/ui/app/serializers/job-scale.js @@ -0,0 +1,19 @@ +import { assign } from '@ember/polyfills'; +import ApplicationSerializer from './application'; + +export default class JobScale extends ApplicationSerializer { + normalize(modelClass, hash) { + // Transform the map-based TaskGroups object into an array-based + // TaskGroupScale fragment list + hash.PlainJobId = hash.JobID; + hash.ID = JSON.stringify([hash.JobID, hash.Namespace || 'default']); + hash.JobID = hash.ID; + + const taskGroups = hash.TaskGroups || {}; + hash.TaskGroupScales = Object.keys(taskGroups).map(key => { + return assign(taskGroups[key], { Name: key }); + }); + + return super.normalize(modelClass, hash); + } +} diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index ded935c0036..7273141d09d 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -84,6 +84,11 @@ export default class JobSerializer extends ApplicationSerializer { related: buildURL(`${jobURL}/evaluations`, { namespace }), }, }, + scale: { + links: { + related: buildURL(`${jobURL}/scale`, { namespace }), + }, + }, }); } } diff --git a/ui/app/serializers/scale-event.js b/ui/app/serializers/scale-event.js new file mode 100644 index 00000000000..758ec304082 --- /dev/null +++ b/ui/app/serializers/scale-event.js @@ -0,0 +1,10 @@ +import ApplicationSerializer from './application'; + +export default class ScaleEventSerializer extends ApplicationSerializer { + normalize(typeHash, hash) { + hash.TimeNanos = hash.Time % 1000000; + hash.Time = Math.floor(hash.Time / 1000000); + + return super.normalize(typeHash, hash); + } +} From 5f2dee8225ef340ea6887eed8bce76478b70ab0d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 24 Jul 2020 21:36:43 -0700 Subject: [PATCH 02/15] Model job scaling and events in Mirage --- ui/mirage/config.js | 9 ++++++++ ui/mirage/factories/job-scale.js | 26 +++++++++++++++++++++++ ui/mirage/factories/job.js | 11 ++++++++++ ui/mirage/factories/scale-event.js | 17 +++++++++++++++ ui/mirage/factories/task-group-scale.js | 24 +++++++++++++++++++++ ui/mirage/models/job-scale.js | 6 ++++++ ui/mirage/models/job.js | 1 + ui/mirage/models/scale-event.js | 5 +++++ ui/mirage/models/task-group-scale.js | 6 ++++++ ui/mirage/serializers/job-scale.js | 6 ++++++ ui/mirage/serializers/task-group-scale.js | 6 ++++++ 11 files changed, 117 insertions(+) create mode 100644 ui/mirage/factories/job-scale.js create mode 100644 ui/mirage/factories/scale-event.js create mode 100644 ui/mirage/factories/task-group-scale.js create mode 100644 ui/mirage/models/job-scale.js create mode 100644 ui/mirage/models/scale-event.js create mode 100644 ui/mirage/models/task-group-scale.js create mode 100644 ui/mirage/serializers/job-scale.js create mode 100644 ui/mirage/serializers/task-group-scale.js diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 80c521791de..c76fa2e4e22 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -157,6 +157,15 @@ export default function() { return deployment ? this.serialize(deployment) : new Response(200, {}, 'null'); }); + this.get( + '/job/:id/scale', + withBlockingSupport(function({ jobScales }, { params }) { + const obj = jobScales.findBy({ jobId: params.id }); + console.log('Job Scale Object', obj); + return this.serialize(jobScales.findBy({ jobId: params.id })); + }) + ); + this.post('/job/:id/periodic/force', function(schema, { params }) { // Create the child job const parent = schema.jobs.find(params.id); diff --git a/ui/mirage/factories/job-scale.js b/ui/mirage/factories/job-scale.js new file mode 100644 index 00000000000..168f2c0e9e8 --- /dev/null +++ b/ui/mirage/factories/job-scale.js @@ -0,0 +1,26 @@ +import { Factory, trait } from 'ember-cli-mirage'; +import faker from 'nomad-ui/mirage/faker'; + +export default Factory.extend({ + groupNames: [], + + jobId: '', + JobID() { + return this.jobId; + }, + namespace: null, + shallow: false, + + afterCreate(jobScale, server) { + const groups = jobScale.groupNames.map(group => + server.create('task-group-scale', { + id: group, + shallow: jobScale.shallow, + }) + ); + + jobScale.update({ + taskGroupScaleIds: groups.mapBy('id'), + }); + }, +}); diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index 90e2f27b394..c09eae9d855 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -158,6 +158,17 @@ export default Factory.extend({ job_summary_id: jobSummary.id, }); + const jobScale = server.create('job-scale', { + groupNames: groups.mapBy('name'), + jobId: job.id, + namespace: job.namespace, + shallow: job.shallow, + }); + + job.update({ + jobScaleId: jobScale.id, + }); + if (!job.noDeployments) { Array(faker.random.number({ min: 1, max: 3 })) .fill(null) diff --git a/ui/mirage/factories/scale-event.js b/ui/mirage/factories/scale-event.js new file mode 100644 index 00000000000..8532c4ec25f --- /dev/null +++ b/ui/mirage/factories/scale-event.js @@ -0,0 +1,17 @@ +import { Factory, trait } from 'ember-cli-mirage'; +import faker from 'nomad-ui/mirage/faker'; + +const REF_TIME = new Date(); + +export default Factory.extend({ + time: () => faker.date.past(2 / 365, REF_TIME) * 1000000, + count: () => faker.random.number(10), + previousCount: () => faker.random.number(10), + error: () => faker.random.number(10) > 9, + message: 'Sample message for a job scale event', + meta: () => ({ + 'nomad_autoscaler.count.capped': true, + 'nomad_autoscaler.count.original': 0, + 'nomad_autoscaler.reason_history': ['scaling down because factor is 0.000000'], + }), +}); diff --git a/ui/mirage/factories/task-group-scale.js b/ui/mirage/factories/task-group-scale.js new file mode 100644 index 00000000000..98160b49d2b --- /dev/null +++ b/ui/mirage/factories/task-group-scale.js @@ -0,0 +1,24 @@ +import { Factory, trait } from 'ember-cli-mirage'; +import faker from 'nomad-ui/mirage/faker'; + +export default Factory.extend({ + name: id => id, + + desired: 1, + placed: 1, + running: 1, + healthy: 1, + unhealthy: 1, + + shallow: false, + + afterCreate(taskGroupScale, server) { + if (!taskGroupScale.shallow) { + const events = server.createList('scale-event', faker.random.number({ min: 1, max: 10 })); + + taskGroupScale.update({ + eventIds: events.mapBy('id'), + }); + } + }, +}); diff --git a/ui/mirage/models/job-scale.js b/ui/mirage/models/job-scale.js new file mode 100644 index 00000000000..8e06eead060 --- /dev/null +++ b/ui/mirage/models/job-scale.js @@ -0,0 +1,6 @@ +import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + job: belongsTo(), + taskGroupScales: hasMany(), +}); diff --git a/ui/mirage/models/job.js b/ui/mirage/models/job.js index 3a69c028164..9c41e610d8b 100644 --- a/ui/mirage/models/job.js +++ b/ui/mirage/models/job.js @@ -3,4 +3,5 @@ import { Model, hasMany, belongsTo } from 'ember-cli-mirage'; export default Model.extend({ task_groups: hasMany('task-group'), job_summary: belongsTo('job-summary'), + job_scale: belongsTo('job-scale'), }); diff --git a/ui/mirage/models/scale-event.js b/ui/mirage/models/scale-event.js new file mode 100644 index 00000000000..3c41f3573fa --- /dev/null +++ b/ui/mirage/models/scale-event.js @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + taskGroupScale: belongsTo(), +}); diff --git a/ui/mirage/models/task-group-scale.js b/ui/mirage/models/task-group-scale.js new file mode 100644 index 00000000000..5262995032a --- /dev/null +++ b/ui/mirage/models/task-group-scale.js @@ -0,0 +1,6 @@ +import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + jobScale: belongsTo(), + events: hasMany('scale-event'), +}); diff --git a/ui/mirage/serializers/job-scale.js b/ui/mirage/serializers/job-scale.js new file mode 100644 index 00000000000..83abf7011a4 --- /dev/null +++ b/ui/mirage/serializers/job-scale.js @@ -0,0 +1,6 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['taskGroupScales'], +}); diff --git a/ui/mirage/serializers/task-group-scale.js b/ui/mirage/serializers/task-group-scale.js new file mode 100644 index 00000000000..506c19038f8 --- /dev/null +++ b/ui/mirage/serializers/task-group-scale.js @@ -0,0 +1,6 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['events'], +}); From 5f2cdd17c17d4f3aa31d2f653a10b7c937c50c2a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 24 Jul 2020 16:37:29 -0700 Subject: [PATCH 03/15] Refactor job summary adapter to use the namespace ID base class --- ui/app/adapters/job-summary.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/ui/app/adapters/job-summary.js b/ui/app/adapters/job-summary.js index 569311c6695..7151b210095 100644 --- a/ui/app/adapters/job-summary.js +++ b/ui/app/adapters/job-summary.js @@ -1,12 +1,3 @@ -import Watchable from './watchable'; +import WatchableNamespaceIDs from './watchable-namespace-ids'; -export default class JobSummaryAdapter extends Watchable { - urlForFindRecord(id, type, hash) { - const [name, namespace] = JSON.parse(id); - let url = super.urlForFindRecord(name, 'job', hash) + '/summary'; - if (namespace && namespace !== 'default') { - url += `?namespace=${namespace}`; - } - return url; - } -} +export default class JobSummaryAdapter extends WatchableNamespaceIDs {} From 87a67023f9d02f8f9be07feffa419c1bf711b7d9 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 27 Jul 2020 21:24:14 -0700 Subject: [PATCH 04/15] New fluid height modifier for the json viewer --- ui/app/components/json-viewer.js | 3 ++- ui/app/styles/components.scss | 1 + ui/app/styles/components/json-viewer.scss | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 ui/app/styles/components/json-viewer.scss diff --git a/ui/app/components/json-viewer.js b/ui/app/components/json-viewer.js index bcb80309940..148f3d70a6b 100644 --- a/ui/app/components/json-viewer.js +++ b/ui/app/components/json-viewer.js @@ -1,10 +1,11 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; -import { classNames } from '@ember-decorators/component'; +import { classNames, classNameBindings } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; @classic @classNames('json-viewer') +@classNameBindings('fluidHeight:has-fluid-height') export default class JsonViewer extends Component { json = null; diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index ae08d6addb7..ea66946de4a 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -18,6 +18,7 @@ @import './components/image-file.scss'; @import './components/inline-definitions'; @import './components/job-diff'; +@import './components/json-viewer'; @import './components/lifecycle-chart'; @import './components/loading-spinner'; @import './components/metrics'; diff --git a/ui/app/styles/components/json-viewer.scss b/ui/app/styles/components/json-viewer.scss new file mode 100644 index 00000000000..68b322c7977 --- /dev/null +++ b/ui/app/styles/components/json-viewer.scss @@ -0,0 +1,5 @@ +.json-viewer { + &.has-fluid-height .CodeMirror-scroll { + min-height: 0; + } +} From 9d43e66c99caf5efa8a8d6df92e14ba549f83872 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 27 Jul 2020 21:26:17 -0700 Subject: [PATCH 05/15] Finish modeling behaviors within job scale events --- ui/app/models/scale-event.js | 11 +++++++++++ ui/app/models/task-group-scale.js | 6 ++++++ ui/app/serializers/job.js | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ui/app/models/scale-event.js b/ui/app/models/scale-event.js index ecab7bc4643..0de1794563b 100644 --- a/ui/app/models/scale-event.js +++ b/ui/app/models/scale-event.js @@ -1,3 +1,4 @@ +import { computed } from '@ember/object'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; @@ -10,9 +11,19 @@ export default class ScaleEvent extends Fragment { @attr('boolean') error; @attr('string') evalId; + @computed('count', 'previousCount', function() { + return this.count > this.previousCount; + }) + increased; + @attr('date') time; @attr('number') timeNanos; @attr('string') message; @attr() meta; + + @computed('meta', function() { + return Object.keys(this.meta).length > 0; + }) + hasMeta; } diff --git a/ui/app/models/task-group-scale.js b/ui/app/models/task-group-scale.js index 83fa05d2efe..dbc307d5fb4 100644 --- a/ui/app/models/task-group-scale.js +++ b/ui/app/models/task-group-scale.js @@ -1,3 +1,4 @@ +import { computed } from '@ember/object'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; @@ -14,4 +15,9 @@ export default class TaskGroupScale extends Fragment { @attr('number') unhealthy; @fragmentArray('scale-event') events; + + @computed('events.length', function() { + return this.events.length; + }) + isVisible; } diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index 7273141d09d..d7f8c760ad7 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -84,7 +84,7 @@ export default class JobSerializer extends ApplicationSerializer { related: buildURL(`${jobURL}/evaluations`, { namespace }), }, }, - scale: { + scaleState: { links: { related: buildURL(`${jobURL}/scale`, { namespace }), }, From 4e01b2c35ebd6784cacbb6538b6130e2efe19142 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 27 Jul 2020 21:27:03 -0700 Subject: [PATCH 06/15] Load and watch the job scale endpoint on the task group page --- ui/app/routes/jobs/job/task-group.js | 18 ++++++++++-------- ui/tests/acceptance/task-group-detail-test.js | 4 +++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ui/app/routes/jobs/job/task-group.js b/ui/app/routes/jobs/job/task-group.js index 72c130c7edd..4921f57ca10 100644 --- a/ui/app/routes/jobs/job/task-group.js +++ b/ui/app/routes/jobs/job/task-group.js @@ -1,7 +1,7 @@ import Route from '@ember/routing/route'; import { collect } from '@ember/object/computed'; import EmberError from '@ember/error'; -import { resolve } from 'rsvp'; +import { resolve, all } from 'rsvp'; import { watchRecord, watchRelationship } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; import { qpBuilder } from 'nomad-ui/utils/classes/query-params'; @@ -43,10 +43,9 @@ export default class TaskGroupRoute extends Route.extend(WithWatchers) { } // Refresh job allocations before-hand (so page sort works on load) - return job - .hasMany('allocations') - .reload() - .then(() => taskGroup); + return all([job.hasMany('allocations').reload(), job.get('scaleState')]).then( + () => taskGroup + ); }) .catch(notifyError(this)); } @@ -56,7 +55,8 @@ export default class TaskGroupRoute extends Route.extend(WithWatchers) { const job = model.get('job'); controller.set('watchers', { job: this.watchJob.perform(job), - summary: this.watchSummary.perform(job.get('summary')), + summary: this.watchSummary.perform(job), + scale: this.watchScale.perform(job), allocations: this.watchAllocations.perform(job), latestDeployment: job.get('supportsDeployments') && this.watchLatestDeployment.perform(job), }); @@ -64,9 +64,11 @@ export default class TaskGroupRoute extends Route.extend(WithWatchers) { } @watchRecord('job') watchJob; - @watchRecord('job-summary') watchSummary; + @watchRelationship('job-summary') watchSummary; + @watchRelationship('job-scale') watchScale; @watchRelationship('allocations') watchAllocations; @watchRelationship('latestDeployment') watchLatestDeployment; - @collect('watchJob', 'watchSummary', 'watchAllocations', 'watchLatestDeployment') watchers; + @collect('watchJob', 'watchSummary', 'watchScale', 'watchAllocations', 'watchLatestDeployment') + watchers; } diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js index 3f667bd3f4c..c91b0ced49b 100644 --- a/ui/tests/acceptance/task-group-detail-test.js +++ b/ui/tests/acceptance/task-group-detail-test.js @@ -329,7 +329,9 @@ module('Acceptance | task group detail', function(hooks) { await TaskGroup.countStepper.increment.click(); await settled(); - const scaleRequest = server.pretender.handledRequests.find(req => req.url.endsWith('/scale')); + const scaleRequest = server.pretender.handledRequests.find( + req => req.method === 'POST' && req.url.endsWith('/scale') + ); const requestBody = JSON.parse(scaleRequest.requestBody); assert.equal(requestBody.Target.Group, scalingGroup.name); assert.equal(requestBody.Count, scalingGroup.count + 1); From 9c5a2b5dd58e48a8022015a1b85f8fbf4f1787ed Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 27 Jul 2020 21:28:37 -0700 Subject: [PATCH 07/15] Present scaling events on the job task group page --- ui/app/styles/components/accordion.scss | 8 ++-- .../styles/components/inline-definitions.scss | 12 ++++++ .../list-accordion/accordion-body.hbs | 2 +- ui/app/templates/jobs/job/task-group.hbs | 41 +++++++++++++++++++ 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/ui/app/styles/components/accordion.scss b/ui/app/styles/components/accordion.scss index 3aaa057e5c4..2b59864a2cd 100644 --- a/ui/app/styles/components/accordion.scss +++ b/ui/app/styles/components/accordion.scss @@ -15,6 +15,10 @@ border-bottom-left-radius: $radius; border-bottom-right-radius: $radius; } + + &.is-full-bleed { + padding: 0; + } } .accordion-head { @@ -26,10 +30,6 @@ background: $white; } - &.is-inactive { - color: $grey-light; - } - .accordion-head-content { width: 100%; margin-right: 1.5em; diff --git a/ui/app/styles/components/inline-definitions.scss b/ui/app/styles/components/inline-definitions.scss index a7927391986..50f72b90543 100644 --- a/ui/app/styles/components/inline-definitions.scss +++ b/ui/app/styles/components/inline-definitions.scss @@ -8,6 +8,10 @@ font-weight: $weight-semibold; } + &.is-faded { + color: darken($grey-blue, 20%); + } + .pair { margin-right: 2em; white-space: nowrap; @@ -27,6 +31,14 @@ } } + .icon-field { + display: flex; + margin-left: -1em; + .icon-container { + width: 1.5em; + } + } + &.is-small { font-size: $size-7; } diff --git a/ui/app/templates/components/list-accordion/accordion-body.hbs b/ui/app/templates/components/list-accordion/accordion-body.hbs index 45371404ec8..19d1aefd626 100644 --- a/ui/app/templates/components/list-accordion/accordion-body.hbs +++ b/ui/app/templates/components/list-accordion/accordion-body.hbs @@ -1,5 +1,5 @@ {{#if this.isOpen}} -
+
{{yield}}
{{/if}} diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index c5a2baf4a04..c92f3416c37 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -137,6 +137,47 @@ + {{#if this.model.scaleState.isVisible}} + {{! this is where the accordion goes }} +
+
+ Recent Scaling Events +
+
+ + +
+
+ + + {{#if a.item.error}}{{x-icon "cancel-circle-fill" class="is-danger"}}{{/if}} + + {{format-ts a.item.time}} + +
+
+ {{#if a.item.count}} + {{#if a.item.increased}} + {{x-icon "arrow-up" class="is-danger"}} + {{else}} + {{x-icon "arrow-down" class="is-primary"}} + {{/if}} + {{a.item.count}} + {{/if}} +
+
+ {{a.item.message}} +
+
+
+ + + +
+
+
+ {{/if}} + {{#if this.model.volumes.length}}
From 9008694ab7b62097e5f5fe1d8cc02db4f4128e4e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 27 Jul 2020 21:29:22 -0700 Subject: [PATCH 08/15] Make scale event properties more conditional and serialized correctly --- ui/mirage/config.js | 1 - ui/mirage/factories/scale-event.js | 15 +++++++++------ ui/mirage/factories/task-group-scale.js | 4 +++- ui/mirage/serializers/job-scale.js | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/ui/mirage/config.js b/ui/mirage/config.js index c76fa2e4e22..c85d4c15c05 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -161,7 +161,6 @@ export default function() { '/job/:id/scale', withBlockingSupport(function({ jobScales }, { params }) { const obj = jobScales.findBy({ jobId: params.id }); - console.log('Job Scale Object', obj); return this.serialize(jobScales.findBy({ jobId: params.id })); }) ); diff --git a/ui/mirage/factories/scale-event.js b/ui/mirage/factories/scale-event.js index 8532c4ec25f..eeb7399d34a 100644 --- a/ui/mirage/factories/scale-event.js +++ b/ui/mirage/factories/scale-event.js @@ -7,11 +7,14 @@ export default Factory.extend({ time: () => faker.date.past(2 / 365, REF_TIME) * 1000000, count: () => faker.random.number(10), previousCount: () => faker.random.number(10), - error: () => faker.random.number(10) > 9, + error: () => faker.random.number(10) > 8, message: 'Sample message for a job scale event', - meta: () => ({ - 'nomad_autoscaler.count.capped': true, - 'nomad_autoscaler.count.original': 0, - 'nomad_autoscaler.reason_history': ['scaling down because factor is 0.000000'], - }), + meta: () => + faker.random.number(10) < 8 + ? { + 'nomad_autoscaler.count.capped': true, + 'nomad_autoscaler.count.original': 0, + 'nomad_autoscaler.reason_history': ['scaling down because factor is 0.000000'], + } + : {}, }); diff --git a/ui/mirage/factories/task-group-scale.js b/ui/mirage/factories/task-group-scale.js index 98160b49d2b..a7f20fc6109 100644 --- a/ui/mirage/factories/task-group-scale.js +++ b/ui/mirage/factories/task-group-scale.js @@ -2,7 +2,9 @@ import { Factory, trait } from 'ember-cli-mirage'; import faker from 'nomad-ui/mirage/faker'; export default Factory.extend({ - name: id => id, + name() { + return this.id; + }, desired: 1, placed: 1, diff --git a/ui/mirage/serializers/job-scale.js b/ui/mirage/serializers/job-scale.js index 83abf7011a4..e01e3df70a4 100644 --- a/ui/mirage/serializers/job-scale.js +++ b/ui/mirage/serializers/job-scale.js @@ -1,6 +1,21 @@ import ApplicationSerializer from './application'; +import { arrToObj } from '../utils'; export default ApplicationSerializer.extend({ embed: true, include: ['taskGroupScales'], + + serialize() { + var json = ApplicationSerializer.prototype.serialize.apply(this, arguments); + if (json instanceof Array) { + json.forEach(serializeJobScale); + } else { + serializeJobScale(json); + } + return json; + }, }); + +function serializeJobScale(jobScale) { + jobScale.TaskGroups = jobScale.TaskGroupScales.reduce(arrToObj('Name'), {}); +} From cc44bc41e5ff092b121aae13b6b953ae0794d2c7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 27 Jul 2020 23:18:10 -0700 Subject: [PATCH 09/15] Acceptance tests for scaling events --- ui/app/controllers/jobs/job/task-group.js | 11 +++++- ui/app/templates/jobs/job/task-group.hbs | 25 ++++++------- ui/mirage/models/job.js | 2 +- ui/tests/acceptance/task-group-detail-test.js | 35 +++++++++++++++++++ ui/tests/pages/jobs/job/task-group.js | 16 +++++++++ 5 files changed, 75 insertions(+), 14 deletions(-) diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js index c780f97220e..876945b0aab 100644 --- a/ui/app/controllers/jobs/job/task-group.js +++ b/ui/app/controllers/jobs/job/task-group.js @@ -1,7 +1,7 @@ import { inject as service } from '@ember/service'; import { alias, readOnly } from '@ember/object/computed'; import Controller from '@ember/controller'; -import { action, computed } from '@ember/object'; +import { action, computed, get } from '@ember/object'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; @@ -51,6 +51,15 @@ export default class TaskGroupController extends Controller.extend( @alias('listSorted') listToSearch; @alias('listSearched') sortedAllocations; + @computed('model.scaleState.events.@each.time', function() { + const events = get(this, 'model.scaleState.events'); + if (events) { + return events.sortBy('time').reverse(); + } + return []; + }) + sortedScaleEvents; + @computed('model.job.runningDeployment') get tooltipText() { if (this.can.cannot('scale job')) return "You aren't allowed to scale task groups"; diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index c92f3416c37..226c6538f37 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -138,34 +138,35 @@ {{#if this.model.scaleState.isVisible}} - {{! this is where the accordion goes }}
Recent Scaling Events
- +
- + {{#if a.item.error}}{{x-icon "cancel-circle-fill" class="is-danger"}}{{/if}} - {{format-ts a.item.time}} + {{format-month-ts a.item.time}}
- {{#if a.item.count}} - {{#if a.item.increased}} - {{x-icon "arrow-up" class="is-danger"}} - {{else}} - {{x-icon "arrow-down" class="is-primary"}} - {{/if}} - {{a.item.count}} + {{#if (gte a.item.count 0)}} + + {{#if a.item.increased}} + {{x-icon "arrow-up" class="is-danger"}} + {{else}} + {{x-icon "arrow-down" class="is-primary"}} + {{/if}} + + {{a.item.count}} {{/if}}
-
+
{{a.item.message}}
diff --git a/ui/mirage/models/job.js b/ui/mirage/models/job.js index 9c41e610d8b..da8bc0c0c3a 100644 --- a/ui/mirage/models/job.js +++ b/ui/mirage/models/job.js @@ -3,5 +3,5 @@ import { Model, hasMany, belongsTo } from 'ember-cli-mirage'; export default Model.extend({ task_groups: hasMany('task-group'), job_summary: belongsTo('job-summary'), - job_scale: belongsTo('job-scale'), + jobScale: belongsTo('job-scale'), }); diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js index c91b0ced49b..4f842d05b41 100644 --- a/ui/tests/acceptance/task-group-detail-test.js +++ b/ui/tests/acceptance/task-group-detail-test.js @@ -406,4 +406,39 @@ module('Acceptance | task group detail', function(hooks) { await TaskGroup.visit({ id: job.id, name: taskGroup.name }); }, }); + + test('when a task group has no scaling events, there is no recent scaling events section', async function(assert) { + const taskGroupScale = job.jobScale.taskGroupScales.models.find(m => m.name === taskGroup.name); + taskGroupScale.update({ events: [] }); + taskGroupScale.save(); + + await TaskGroup.visit({ id: job.id, name: taskGroup.name }); + + assert.notOk(TaskGroup.hasScaleEvents); + }); + + test('the recent scaling events section shows all recent scaling events in reverse chronological order', async function(assert) { + const taskGroupScale = job.jobScale.taskGroupScales.models.find(m => m.name === taskGroup.name); + const scaleEvents = taskGroupScale.events.models.sortBy('time').reverse(); + await TaskGroup.visit({ id: job.id, name: taskGroup.name }); + + assert.ok(TaskGroup.hasScaleEvents); + + scaleEvents.forEach((scaleEvent, idx) => { + const ScaleEvent = TaskGroup.scaleEvents[idx]; + assert.equal(ScaleEvent.time, moment(scaleEvent.time / 1000000).format('MMM DD HH:mm:ss ZZ')); + assert.equal(ScaleEvent.message, scaleEvent.message); + assert.equal(ScaleEvent.count, scaleEvent.count); + + if (scaleEvent.error) { + assert.ok(ScaleEvent.error); + } + + if (Object.keys(scaleEvent.meta).length) { + assert.ok(ScaleEvent.isToggleable); + } else { + assert.notOk(ScaleEvent.isToggleable); + } + }); + }); }); diff --git a/ui/tests/pages/jobs/job/task-group.js b/ui/tests/pages/jobs/job/task-group.js index d69b267501f..0bda5a76931 100644 --- a/ui/tests/pages/jobs/job/task-group.js +++ b/ui/tests/pages/jobs/job/task-group.js @@ -53,6 +53,22 @@ export default create({ permissions: text('[data-test-volume-permissions]'), }), + hasScaleEvents: isPresent('[data-test-scale-events]'), + scaleEvents: collection('[data-test-scale-events] [data-test-accordion-head]', { + error: isPresent('[data-test-error]'), + time: text('[data-test-time]'), + count: text('[data-test-count]'), + countIcon: { scope: '[data-test-count-icon]' }, + message: text('[data-test-message]'), + + isToggleable: isPresent('[data-test-accordion-toggle]:not(.is-invisible)'), + toggle: clickable('[data-test-accordion-toggle]'), + }), + + scaleEventBodies: collection('[data-test-scale-events] [data-test-accordion-body]', { + meta: text(), + }), + error: error(), emptyState: { From 193dc7a6970f356b3869a6324a3f4a822dd7bc43 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 28 Jul 2020 09:48:07 -0700 Subject: [PATCH 10/15] Refactor scale events into their own component --- .../components/scale-events-accordion.hbs | 32 ++++++++++++++++++ ui/app/templates/jobs/job/task-group.hbs | 33 +------------------ 2 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 ui/app/templates/components/scale-events-accordion.hbs diff --git a/ui/app/templates/components/scale-events-accordion.hbs b/ui/app/templates/components/scale-events-accordion.hbs new file mode 100644 index 00000000000..f6f1579b7d5 --- /dev/null +++ b/ui/app/templates/components/scale-events-accordion.hbs @@ -0,0 +1,32 @@ + + +
+
+ + + {{#if a.item.error}}{{x-icon "cancel-circle-fill" class="is-danger"}}{{/if}} + + {{format-month-ts a.item.time}} + +
+
+ {{#if (gte a.item.count 0)}} + + {{#if a.item.increased}} + {{x-icon "arrow-up" class="is-danger"}} + {{else}} + {{x-icon "arrow-down" class="is-primary"}} + {{/if}} + + {{a.item.count}} + {{/if}} +
+
+ {{a.item.message}} +
+
+
+ + + +
diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index 226c6538f37..fa4114ff9cd 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -143,38 +143,7 @@ Recent Scaling Events
- - -
-
- - - {{#if a.item.error}}{{x-icon "cancel-circle-fill" class="is-danger"}}{{/if}} - - {{format-month-ts a.item.time}} - -
-
- {{#if (gte a.item.count 0)}} - - {{#if a.item.increased}} - {{x-icon "arrow-up" class="is-danger"}} - {{else}} - {{x-icon "arrow-down" class="is-primary"}} - {{/if}} - - {{a.item.count}} - {{/if}} -
-
- {{a.item.message}} -
-
-
- - - -
+
{{/if}} From b0830f53050956457924b593b9906a6b9b7d77ff Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 28 Jul 2020 09:48:42 -0700 Subject: [PATCH 11/15] Integration tests for the scale-events-accordion component --- ui/app/templates/components/json-viewer.hbs | 1 + .../components/scale-events-accordion-test.js | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 ui/tests/integration/components/scale-events-accordion-test.js diff --git a/ui/app/templates/components/json-viewer.hbs b/ui/app/templates/components/json-viewer.hbs index bdf47e50952..d5f3cc57669 100644 --- a/ui/app/templates/components/json-viewer.hbs +++ b/ui/app/templates/components/json-viewer.hbs @@ -1,4 +1,5 @@ `; + + test('it shows an accordion with an entry for each event', async function(assert) { + const eventCount = 5; + const taskGroup = await this.taskGroupWithEvents(server.createList('scale-event', eventCount)); + this.set('events', taskGroup.scaleState.events); + + await render(commonTemplate); + + assert.equal(findAll('[data-test-scale-events] [data-test-accordion-head]').length, eventCount); + }); + + test('when an event is an error, an error icon is shown', async function(assert) { + const taskGroup = await this.taskGroupWithEvents( + server.createList('scale-event', 1, { error: true }) + ); + this.set('events', taskGroup.scaleState.events); + + await render(commonTemplate); + + assert.ok(find('[data-test-error]')); + }); + + test('when an event has a count higher than previous count, a danger up arrow is shown', async function(assert) { + const count = 5; + const taskGroup = await this.taskGroupWithEvents( + server.createList('scale-event', 1, { count, previousCount: count - 1, error: false }) + ); + this.set('events', taskGroup.scaleState.events); + + await render(commonTemplate); + + assert.notOk(find('[data-test-error]')); + assert.equal(find('[data-test-count]').textContent, count); + assert.ok( + find('[data-test-count-icon]') + .querySelector('.icon') + .classList.contains('is-danger') + ); + }); + + test('when an event has a count lower than previous count, a primary down arrow is shown', async function(assert) { + const count = 5; + const taskGroup = await this.taskGroupWithEvents( + server.createList('scale-event', 1, { count, previousCount: count + 1, error: false }) + ); + this.set('events', taskGroup.scaleState.events); + + await render(commonTemplate); + + assert.notOk(find('[data-test-error]')); + assert.equal(find('[data-test-count]').textContent, count); + assert.ok( + find('[data-test-count-icon]') + .querySelector('.icon') + .classList.contains('is-primary') + ); + }); + + test('when an event has no count, the count is omitted', async function(assert) { + const taskGroup = await this.taskGroupWithEvents( + server.createList('scale-event', 1, { count: null }) + ); + this.set('events', taskGroup.scaleState.events); + + await render(commonTemplate); + + assert.equal(find('[data-test-count]').textContent, ''); + }); + + test('when an event has no meta properties, the accordion entry is not expandable', async function(assert) { + const taskGroup = await this.taskGroupWithEvents( + server.createList('scale-event', 1, { meta: {} }) + ); + this.set('events', taskGroup.scaleState.events); + + await render(commonTemplate); + + assert.ok(find('[data-test-accordion-toggle]').classList.contains('is-invisible')); + }); + + test('when an event has meta properties, the accordion entry is expanding, presenting the meta properties in a json viewer', async function(assert) { + const meta = { + prop: 'one', + prop2: 'two', + deep: { + prop: 'here', + 'dot.separate.prop': 12, + }, + }; + const taskGroup = await this.taskGroupWithEvents(server.createList('scale-event', 1, { meta })); + this.set('events', taskGroup.scaleState.events); + + await render(commonTemplate); + assert.notOk(find('[data-test-accordion-body]')); + + await click('[data-test-accordion-toggle]'); + assert.ok(find('[data-test-accordion-body]')); + + assert.equal( + getCodeMirrorInstance('[data-test-json-viewer]').getValue(), + JSON.stringify(meta, null, 2) + ); + }); +}); From a5031913b04fee406edbc23dc2f18e8571447df7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 28 Jul 2020 11:03:47 -0700 Subject: [PATCH 12/15] Assert that the scale up/down indicator is not shown when the count is null --- ui/app/models/scale-event.js | 5 +++++ ui/app/templates/components/scale-events-accordion.hbs | 2 +- .../integration/components/scale-events-accordion-test.js | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/app/models/scale-event.js b/ui/app/models/scale-event.js index 0de1794563b..e87c2a19cc2 100644 --- a/ui/app/models/scale-event.js +++ b/ui/app/models/scale-event.js @@ -11,6 +11,11 @@ export default class ScaleEvent extends Fragment { @attr('boolean') error; @attr('string') evalId; + @computed('count', function() { + return this.count != null; + }) + hasCount; + @computed('count', 'previousCount', function() { return this.count > this.previousCount; }) diff --git a/ui/app/templates/components/scale-events-accordion.hbs b/ui/app/templates/components/scale-events-accordion.hbs index f6f1579b7d5..dddfd7d6789 100644 --- a/ui/app/templates/components/scale-events-accordion.hbs +++ b/ui/app/templates/components/scale-events-accordion.hbs @@ -10,7 +10,7 @@
- {{#if (gte a.item.count 0)}} + {{#if a.item.hasCount}} {{#if a.item.increased}} {{x-icon "arrow-up" class="is-danger"}} diff --git a/ui/tests/integration/components/scale-events-accordion-test.js b/ui/tests/integration/components/scale-events-accordion-test.js index 34165e7e32d..d003e347511 100644 --- a/ui/tests/integration/components/scale-events-accordion-test.js +++ b/ui/tests/integration/components/scale-events-accordion-test.js @@ -93,7 +93,8 @@ module('Integration | Component | scale-events-accordion', function(hooks) { await render(commonTemplate); - assert.equal(find('[data-test-count]').textContent, ''); + assert.notOk(find('[data-test-count]')); + assert.notOk(find('[data-test-count-icon]')); }); test('when an event has no meta properties, the accordion entry is not expandable', async function(assert) { From f041e081c08fd8ac72274de32402e9001dd64374 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 28 Jul 2020 18:24:24 -0700 Subject: [PATCH 13/15] Fix scale and summary adapters to correct live reloading --- ui/app/adapters/job-scale.js | 6 +++++- ui/app/adapters/job-summary.js | 6 +++++- ui/app/adapters/watchable-namespace-ids.js | 3 ++- ui/app/routes/jobs/job/task-group.js | 8 ++++---- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/ui/app/adapters/job-scale.js b/ui/app/adapters/job-scale.js index 8cca86df718..f068e69a848 100644 --- a/ui/app/adapters/job-scale.js +++ b/ui/app/adapters/job-scale.js @@ -1,3 +1,7 @@ import WatchableNamespaceIDs from './watchable-namespace-ids'; -export default class JobScaleAdapter extends WatchableNamespaceIDs {} +export default class JobScaleAdapter extends WatchableNamespaceIDs { + urlForFindRecord(id, type, hash) { + return super.urlForFindRecord(id, 'job', hash, 'scale'); + } +} diff --git a/ui/app/adapters/job-summary.js b/ui/app/adapters/job-summary.js index 7151b210095..62ce05a5b2d 100644 --- a/ui/app/adapters/job-summary.js +++ b/ui/app/adapters/job-summary.js @@ -1,3 +1,7 @@ import WatchableNamespaceIDs from './watchable-namespace-ids'; -export default class JobSummaryAdapter extends WatchableNamespaceIDs {} +export default class JobSummaryAdapter extends WatchableNamespaceIDs { + urlForFindRecord(id, type, hash) { + return super.urlForFindRecord(id, 'job', hash, 'summary'); + } +} diff --git a/ui/app/adapters/watchable-namespace-ids.js b/ui/app/adapters/watchable-namespace-ids.js index 65471283ebe..5ca12a36881 100644 --- a/ui/app/adapters/watchable-namespace-ids.js +++ b/ui/app/adapters/watchable-namespace-ids.js @@ -35,9 +35,10 @@ export default class WatchableNamespaceIDs extends Watchable { return associateNamespace(url, namespace); } - urlForFindRecord(id, type, hash) { + urlForFindRecord(id, type, hash, pathSuffix) { const [name, namespace] = JSON.parse(id); let url = super.urlForFindRecord(name, type, hash); + if (pathSuffix) url += `/${pathSuffix}`; return associateNamespace(url, namespace); } diff --git a/ui/app/routes/jobs/job/task-group.js b/ui/app/routes/jobs/job/task-group.js index 4921f57ca10..e4f1ad9edd5 100644 --- a/ui/app/routes/jobs/job/task-group.js +++ b/ui/app/routes/jobs/job/task-group.js @@ -55,8 +55,8 @@ export default class TaskGroupRoute extends Route.extend(WithWatchers) { const job = model.get('job'); controller.set('watchers', { job: this.watchJob.perform(job), - summary: this.watchSummary.perform(job), - scale: this.watchScale.perform(job), + summary: this.watchSummary.perform(job.get('summary')), + scale: this.watchScale.perform(job.get('scaleState')), allocations: this.watchAllocations.perform(job), latestDeployment: job.get('supportsDeployments') && this.watchLatestDeployment.perform(job), }); @@ -64,8 +64,8 @@ export default class TaskGroupRoute extends Route.extend(WithWatchers) { } @watchRecord('job') watchJob; - @watchRelationship('job-summary') watchSummary; - @watchRelationship('job-scale') watchScale; + @watchRecord('job-summary') watchSummary; + @watchRecord('job-scale') watchScale; @watchRelationship('allocations') watchAllocations; @watchRelationship('latestDeployment') watchLatestDeployment; From a45f316b2562211a1ed1a200d0a24cc9dfdc775e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 29 Jul 2020 19:26:18 -0700 Subject: [PATCH 14/15] Add a tooltip to explain the count change icons --- ui/app/templates/components/scale-events-accordion.hbs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/app/templates/components/scale-events-accordion.hbs b/ui/app/templates/components/scale-events-accordion.hbs index dddfd7d6789..8ed0ec89373 100644 --- a/ui/app/templates/components/scale-events-accordion.hbs +++ b/ui/app/templates/components/scale-events-accordion.hbs @@ -11,7 +11,10 @@
{{#if a.item.hasCount}} - + {{#if a.item.increased}} {{x-icon "arrow-up" class="is-danger"}} {{else}} From 75aa9cebe31f61c1dfafb4f107185dde2848a0b8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 29 Jul 2020 22:27:54 -0700 Subject: [PATCH 15/15] Add scaling observability feature to the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b5612e9598..7394460df3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ FEATURES: * **Multiple Vault Namespaces (Enterprise)**: Support for multiple Vault Namespaces [[GH-8453](https://github.com/hashicorp/nomad/issues/8453)] + * **Scaling Observability UI**: View changes in task group scale (both manual and automatic) over time. [[GH-8551](https://github.com/hashicorp/nomad/issues/8551)] BUG FIXES: