From a7eca1686424b15f76f3ff0ec50e80c57e2516fd Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 7 May 2018 16:53:23 -0700 Subject: [PATCH 01/17] Remove stale dev code This was used to get around direct requests to clients. The UI will now automatically route through the server. --- ui/app/serializers/node.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js index e0ecfc9a7..78bc31f42 100644 --- a/ui/app/serializers/node.js +++ b/ui/app/serializers/node.js @@ -9,12 +9,6 @@ export default ApplicationSerializer.extend({ }, normalize(modelClass, hash) { - // Proxy local agent to the same proxy express server Ember is using - // to avoid CORS - if (this.get('config.isDev') && hash.HTTPAddr === '127.0.0.1:4646') { - hash.HTTPAddr = '127.0.0.1:4200'; - } - return this._super(modelClass, hash); }, From bc6467c6aedfde4969d0e3c59d77e176c413bf5f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 09:39:27 -0700 Subject: [PATCH 02/17] Data modeling for node events and node drivers --- ui/app/models/node-driver.js | 15 +++++++++++++++ ui/app/models/node-event.js | 12 ++++++++++++ ui/app/models/node.js | 5 ++++- ui/app/serializers/node-event.js | 7 +++++++ ui/app/serializers/node.js | 7 +++++++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 ui/app/models/node-driver.js create mode 100644 ui/app/models/node-event.js create mode 100644 ui/app/serializers/node-event.js diff --git a/ui/app/models/node-driver.js b/ui/app/models/node-driver.js new file mode 100644 index 000000000..c656d5281 --- /dev/null +++ b/ui/app/models/node-driver.js @@ -0,0 +1,15 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; +import { fragment } from 'ember-data-model-fragments/attributes'; + +export default Fragment.extend({ + node: fragmentOwner(), + + attributes: fragment('node-attributes'), + name: attr('string'), + detected: attr('boolean', { defaultValue: false }), + healthy: attr('boolean', { defaultValue: false }), + healthDescription: attr('string'), + updateTime: attr('date'), +}); diff --git a/ui/app/models/node-event.js b/ui/app/models/node-event.js new file mode 100644 index 000000000..2e6ee30d5 --- /dev/null +++ b/ui/app/models/node-event.js @@ -0,0 +1,12 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; + +export default Fragment.extend({ + node: fragmentOwner(), + + message: attr('string'), + subsystem: attr('string'), + details: attr(), + time: attr('date'), +}); diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 4b5f34a44..b41613106 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -2,7 +2,7 @@ import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { hasMany } from 'ember-data/relationships'; -import { fragment } from 'ember-data-model-fragments/attributes'; +import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; import shortUUIDProperty from '../utils/properties/short-uuid'; import ipParts from '../utils/ip-parts'; @@ -37,4 +37,7 @@ export default Model.extend({ }), allocations: hasMany('allocations', { inverse: 'node' }), + + drivers: fragmentArray('node-driver'), + events: fragmentArray('node-event'), }); diff --git a/ui/app/serializers/node-event.js b/ui/app/serializers/node-event.js new file mode 100644 index 000000000..606c6dadd --- /dev/null +++ b/ui/app/serializers/node-event.js @@ -0,0 +1,7 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + attrs: { + time: 'Timestamp', + }, +}); diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js index 78bc31f42..a8d3410e2 100644 --- a/ui/app/serializers/node.js +++ b/ui/app/serializers/node.js @@ -1,3 +1,5 @@ +import { get } from '@ember/object'; +import { assign } from '@ember/polyfills'; import { inject as service } from '@ember/service'; import ApplicationSerializer from './application'; @@ -9,6 +11,11 @@ export default ApplicationSerializer.extend({ }, normalize(modelClass, hash) { + // Transform the map-based Drivers object into an array-based NodeDriver fragment list + hash.Drivers = Object.keys(get(hash, 'Drivers') || {}).map(key => { + return assign({}, get(hash, `Drivers.${key}`), { Name: key }); + }); + return this._super(modelClass, hash); }, From d2810bbaf4145e4d0e242902a59d377aaaf143a1 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 11:22:36 -0700 Subject: [PATCH 03/17] Show driver summary on the client detail page --- ui/app/models/node.js | 8 ++++++++ ui/app/templates/clients/client.hbs | 24 +++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/ui/app/models/node.js b/ui/app/models/node.js index b41613106..40c68e376 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -40,4 +40,12 @@ export default Model.extend({ drivers: fragmentArray('node-driver'), events: fragmentArray('node-event'), + + detectedDrivers: computed('drivers.@each.detected', function() { + return this.get('drivers').filterBy('detected'); + }), + + unhealthyDrivers: computed('detectedDrivers.@each.healthy', function() { + return this.get('detectedDrivers').filterBy('healthy', false); + }), }); diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index b9ec625aa..57f18e3f1 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -17,9 +17,27 @@
Client Details - Status {{model.status}} - Address {{model.httpAddr}} - Datacenter {{model.datacenter}} + + Status + {{model.status}} + + + Address + {{model.httpAddr}} + + + Datacenter + {{model.datacenter}} + + + Drivers + {{#if model.unhealthyDrivers.length}} + {{x-icon "warning" class="is-text is-warning"}} + {{model.unhealthyDrivers.length}} of {{model.detectedDrivers.length}} {{pluralize "driver" model.detectedDrivers.length}} unhealthy + {{else}} + All healthy + {{/if}} +
From 76caeb508308a27828625dcfbc1483fc714d93f3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 11:23:42 -0700 Subject: [PATCH 04/17] Add a node events section to the node detail page --- ui/app/controllers/clients/client.js | 6 ++++++ ui/app/templates/clients/client.hbs | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 008169f5e..6e2000116 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -24,6 +24,12 @@ export default Controller.extend(Sortable, Searchable, { listToSearch: alias('listSorted'), sortedAllocations: alias('listSearched'), + sortedEvents: computed('model.events.@each.time', function() { + return this.get('model.events') + .sortBy('time') + .reverse(); + }), + actions: { gotoAllocation(allocation) { this.transitionToRoute('allocations.allocation', allocation); diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 57f18e3f1..5a9ee7f78 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -93,6 +93,34 @@ +
+
+ Client Events +
+
+ {{#list-table source=sortedEvents class="is-striped" as |t|}} + {{#t.head}} + Time + Subsystem + Message + {{/t.head}} + {{#t.body as |row|}} + + {{moment-format row.model.time "MM/DD/YY HH:mm:ss"}} + {{row.model.subsystem}} + + {{#if row.model.message}} + {{row.model.message}} + {{else}} + No message + {{/if}} + + + {{/t.body}} + {{/list-table}} +
+
+
Attributes From a022b618f4c74fa258cbc4d0da862fca7c01740f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 11:24:11 -0700 Subject: [PATCH 05/17] Show a warning icon on client node rows that have unhealthy drivers --- ui/app/templates/clients/index.hbs | 1 + ui/app/templates/components/client-node-row.hbs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/ui/app/templates/clients/index.hbs b/ui/app/templates/clients/index.hbs index 1b43c78c3..888c05634 100644 --- a/ui/app/templates/clients/index.hbs +++ b/ui/app/templates/clients/index.hbs @@ -23,6 +23,7 @@ sortDescending=sortDescending class="with-foot" as |t|}} {{#t.head}} + {{#t.sort-by prop="id"}}ID{{/t.sort-by}} {{#t.sort-by class="is-200px is-truncatable" prop="name"}}Name{{/t.sort-by}} {{#t.sort-by prop="status"}}Status{{/t.sort-by}} diff --git a/ui/app/templates/components/client-node-row.hbs b/ui/app/templates/components/client-node-row.hbs index 184a00e2b..9a946006d 100644 --- a/ui/app/templates/components/client-node-row.hbs +++ b/ui/app/templates/components/client-node-row.hbs @@ -1,3 +1,10 @@ + + {{#if node.unhealthyDrivers.length}} + + {{x-icon "warning" class="is-warning"}} + + {{/if}} + {{#link-to "clients.client" node.id class="is-primary"}}{{node.shortId}}{{/link-to}} {{node.name}} {{node.status}} From 53244e24624c76a3d1f89062438a1244236ab876 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 18:08:30 -0700 Subject: [PATCH 06/17] New accordion component Follows the same style as the table and pagination components. --- ui/app/components/list-accordion.js | 30 +++++++++++++ .../list-accordion/accordion-body.js | 6 +++ .../list-accordion/accordion-head.js | 14 +++++++ ui/app/styles/components.scss | 1 + ui/app/styles/components/accordion.scss | 42 +++++++++++++++++++ .../templates/components/list-accordion.hbs | 10 +++++ .../list-accordion/accordion-body.hbs | 5 +++ .../list-accordion/accordion-head.hbs | 8 ++++ 8 files changed, 116 insertions(+) create mode 100644 ui/app/components/list-accordion.js create mode 100644 ui/app/components/list-accordion/accordion-body.js create mode 100644 ui/app/components/list-accordion/accordion-head.js create mode 100644 ui/app/styles/components/accordion.scss create mode 100644 ui/app/templates/components/list-accordion.hbs create mode 100644 ui/app/templates/components/list-accordion/accordion-body.hbs create mode 100644 ui/app/templates/components/list-accordion/accordion-head.hbs diff --git a/ui/app/components/list-accordion.js b/ui/app/components/list-accordion.js new file mode 100644 index 000000000..9e004645d --- /dev/null +++ b/ui/app/components/list-accordion.js @@ -0,0 +1,30 @@ +import Component from '@ember/component'; +import { computed, get } from '@ember/object'; + +export default Component.extend({ + classNames: ['accordion'], + + key: 'id', + source: computed(() => []), + + decoratedSource: computed('source.[]', function() { + const stateCache = this.get('stateCache'); + const key = this.get('key'); + const deepKey = `item.${key}`; + + const decoratedSource = this.get('source').map(item => { + const cacheItem = stateCache.findBy(deepKey, get(item, key)); + return { + item, + isOpen: cacheItem ? !!cacheItem.isOpen : false, + }; + }); + + this.set('stateCache', decoratedSource); + return decoratedSource; + }), + + // When source updates come in, the state cache is used to preserve + // open/close state. + stateCache: computed(() => []), +}); diff --git a/ui/app/components/list-accordion/accordion-body.js b/ui/app/components/list-accordion/accordion-body.js new file mode 100644 index 000000000..32397fce6 --- /dev/null +++ b/ui/app/components/list-accordion/accordion-body.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', + isOpen: false, +}); diff --git a/ui/app/components/list-accordion/accordion-head.js b/ui/app/components/list-accordion/accordion-head.js new file mode 100644 index 000000000..4de6d4edc --- /dev/null +++ b/ui/app/components/list-accordion/accordion-head.js @@ -0,0 +1,14 @@ +import Component from '@ember/component'; + +export default Component.extend({ + classNames: ['accordion-head'], + classNameBindings: ['isOpen::is-light', 'isExpandable::is-inactive'], + + buttonLabel: 'toggle', + isOpen: false, + isExpandable: true, + item: null, + + onClose() {}, + onOpen() {}, +}); diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index 49b513806..83efbb676 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -1,3 +1,4 @@ +@import './components/accordion'; @import './components/badge'; @import './components/boxed-section'; @import './components/cli-window'; diff --git a/ui/app/styles/components/accordion.scss b/ui/app/styles/components/accordion.scss new file mode 100644 index 000000000..17ca8bcc5 --- /dev/null +++ b/ui/app/styles/components/accordion.scss @@ -0,0 +1,42 @@ +.accordion { + .accordion-head, + .accordion-body { + border: 1px solid $grey-blue; + border-bottom: none; + padding: 0.75em 1.5em; + + &:first-child { + border-top-left-radius: $radius; + border-top-right-radius: $radius; + } + + &:last-child { + border-bottom: 1px solid $grey-blue; + border-bottom-left-radius: $radius; + border-bottom-right-radius: $radius; + } + } + + .accordion-head { + display: flex; + background: $white-ter; + flex: 1; + + &.is-light { + background: $white; + } + + &.is-inactive { + color: $grey-light; + } + + .accordion-head-content { + width: 100%; + } + + .accordion-toggle { + flex-basis: 0; + white-space: nowrap; + } + } +} diff --git a/ui/app/templates/components/list-accordion.hbs b/ui/app/templates/components/list-accordion.hbs new file mode 100644 index 000000000..67678cef2 --- /dev/null +++ b/ui/app/templates/components/list-accordion.hbs @@ -0,0 +1,10 @@ +{{#each decoratedSource as |item|}} + {{yield (hash + head=(component "list-accordion/accordion-head" + isOpen=item.isOpen + onOpen=(action (mut item.isOpen) true) + onClose=(action (mut item.isOpen) false)) + body=(component "list-accordion/accordion-body" isOpen=item.isOpen) + item=item.item + )}} +{{/each}} diff --git a/ui/app/templates/components/list-accordion/accordion-body.hbs b/ui/app/templates/components/list-accordion/accordion-body.hbs new file mode 100644 index 000000000..894e66818 --- /dev/null +++ b/ui/app/templates/components/list-accordion/accordion-body.hbs @@ -0,0 +1,5 @@ +{{#if isOpen}} +
+ {{yield}} +
+{{/if}} diff --git a/ui/app/templates/components/list-accordion/accordion-head.hbs b/ui/app/templates/components/list-accordion/accordion-head.hbs new file mode 100644 index 000000000..a362b1165 --- /dev/null +++ b/ui/app/templates/components/list-accordion/accordion-head.hbs @@ -0,0 +1,8 @@ +
+ {{yield}} +
+ From 8fa044a52a79ac99e895f000be3f89c2c6bac6a3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 18:09:20 -0700 Subject: [PATCH 07/17] Add driver status accordion section to the client detail page --- ui/app/controllers/clients/client.js | 4 ++ ui/app/models/node-driver.js | 5 +++ ui/app/templates/clients/client.hbs | 58 ++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 6e2000116..26dfc9b64 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -30,6 +30,10 @@ export default Controller.extend(Sortable, Searchable, { .reverse(); }), + sortedDrivers: computed('model.drivers.@each.name', function() { + return this.get('model.drivers').sortBy('name'); + }), + actions: { gotoAllocation(allocation) { this.transitionToRoute('allocations.allocation', allocation); diff --git a/ui/app/models/node-driver.js b/ui/app/models/node-driver.js index c656d5281..d412867e7 100644 --- a/ui/app/models/node-driver.js +++ b/ui/app/models/node-driver.js @@ -1,4 +1,5 @@ import Fragment from 'ember-data-model-fragments/fragment'; +import { computed } from '@ember/object'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; import { fragment } from 'ember-data-model-fragments/attributes'; @@ -12,4 +13,8 @@ export default Fragment.extend({ healthy: attr('boolean', { defaultValue: false }), healthDescription: attr('string'), updateTime: attr('date'), + + healthClass: computed('healthy', function() { + return this.get('healthy') ? 'running' : 'failed'; + }), }); diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 5a9ee7f78..79a18e34c 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -121,6 +121,64 @@
+
+
+ Driver Status +
+
+ {{#list-accordion source=sortedDrivers key="name" as |a|}} + {{#a.head buttonLabel="details" isExpandable=a.item.detected}} +
+
+ {{a.item.name}} +
+
+ {{#if a.item.detected}} + + + {{if a.item.healthy "Healthy" "Unhealthy"}} + + {{/if}} +
+
+ + Detected + {{if a.item.detected "Yes" "No"}} + + + + Last Updated + {{moment-from-now a.item.updateTime interval=1000}} + + +
+
+ {{/a.head}} + {{#a.body}} +

{{a.item.healthDescription}}

+
+
+ {{capitalize a.item.name}} Attributes +
+ {{#if a.item.attributes.attributesStructured}} +
+ {{attributes-table + attributes=a.item.attributes.attributesStructured + class="attributes-table"}} +
+ {{else}} +
+
+

No Driver Attributes

+
+
+ {{/if}} +
+ {{/a.body}} + {{/list-accordion}} +
+
+
Attributes From 1a55f18085b7358d098a52500dcd80d5e4619e9c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 9 May 2018 17:04:45 -0700 Subject: [PATCH 08/17] Spread the driver health love throughout job models --- ui/app/models/allocation.js | 12 ++++++++++++ ui/app/models/job.js | 22 ++++++++++++++++++++++ ui/app/models/node.js | 4 ++++ ui/app/models/task-group.js | 6 ++++++ ui/app/models/task-state.js | 10 ++++++++++ ui/package.json | 1 + ui/yarn.lock | 4 ++++ 7 files changed, 59 insertions(+) diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 048e31e55..f4dbad07d 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -4,6 +4,7 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo } from 'ember-data/relationships'; import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; +import intersection from 'npm:lodash.intersection'; import shortUUIDProperty from '../utils/properties/short-uuid'; import AllocationStats from '../utils/classes/allocation-stats'; @@ -61,6 +62,17 @@ export default Model.extend({ return taskGroups && taskGroups.findBy('name', this.get('taskGroupName')); }), + unhealthyDrivers: computed('taskGroup.drivers.[]', 'node.unhealthyDriverNames.[]', function() { + const taskGroupUnhealthyDrivers = this.get('taskGroup.drivers'); + const nodeUnhealthyDrivers = this.get('node.unhealthyDriverNames'); + + if (taskGroupUnhealthyDrivers && nodeUnhealthyDrivers) { + return intersection(taskGroupUnhealthyDrivers, nodeUnhealthyDrivers); + } + + return []; + }), + fetchStats() { return this.get('token') .authorizedRequest(`/v1/client/allocation/${this.get('id')}/stats`) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index b8ba9670f..621b869ca 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -116,6 +116,28 @@ export default Model.extend({ evaluations: hasMany('evaluations'), namespace: belongsTo('namespace'), + drivers: computed('taskGroups.@each.drivers', function() { + return this.get('taskGroups') + .mapBy('drivers') + .reduce((all, drivers) => { + all.push(...drivers); + return all; + }, []) + .uniq(); + }), + + // Getting all unhealthy drivers for a job can be incredibly expensive if the job + // has many allocations. This can lead to making an API request for many nodes. + unhealthyDrivers: computed('allocations.@each.unhealthyDrivers.[]', function() { + return this.get('allocations') + .mapBy('unhealthyDrivers') + .reduce((all, drivers) => { + all.push(...drivers); + return all; + }, []) + .uniq(); + }), + hasBlockedEvaluation: computed('evaluations.@each.isBlocked', function() { return this.get('evaluations') .toArray() diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 40c68e376..4dbb4fec6 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -48,4 +48,8 @@ export default Model.extend({ unhealthyDrivers: computed('detectedDrivers.@each.healthy', function() { return this.get('detectedDrivers').filterBy('healthy', false); }), + + unhealthyDriverNames: computed('unhealthyDrivers.@each.name', function() { + return this.get('unhealthyDrivers').mapBy('name'); + }), }); diff --git a/ui/app/models/task-group.js b/ui/app/models/task-group.js index 9be65bdba..ab1a37bbe 100644 --- a/ui/app/models/task-group.js +++ b/ui/app/models/task-group.js @@ -14,6 +14,12 @@ export default Fragment.extend({ tasks: fragmentArray('task'), + drivers: computed('tasks.@each.driver', function() { + return this.get('tasks') + .mapBy('driver') + .uniq(); + }), + allocations: computed('job.allocations.@each.taskGroup', function() { return maybe(this.get('job.allocations')).filterBy('taskGroupName', this.get('name')); }), diff --git a/ui/app/models/task-state.js b/ui/app/models/task-state.js index 217e87459..e0c8918c8 100644 --- a/ui/app/models/task-state.js +++ b/ui/app/models/task-state.js @@ -1,5 +1,6 @@ import { none } from '@ember/object/computed'; import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragment, fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; @@ -19,6 +20,15 @@ export default Fragment.extend({ return tasks && tasks.findBy('name', this.get('name')); }), + driver: alias('task.driver'), + + // TaskState represents a task running on a node, so in addition to knowing the + // driver via the task, the health of the driver is also known via the node + driverStatus: computed('task.driver', 'allocation.node.drivers.[]', function() { + const nodeDrivers = this.get('allocation.node.drivers') || []; + return nodeDrivers.findBy('name', this.get('task.driver')); + }), + resources: fragment('resources'), events: fragmentArray('task-event'), diff --git a/ui/package.json b/ui/package.json index 649c495a3..566b798c9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -72,6 +72,7 @@ "json-formatter-js": "^2.2.0", "lint-staged": "^6.0.0", "loader.js": "^4.2.3", + "lodash.intersection": "^4.4.0", "prettier": "^1.4.4", "query-string": "^5.0.0" }, diff --git a/ui/yarn.lock b/ui/yarn.lock index 6f921b680..795e0cf81 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -5691,6 +5691,10 @@ lodash.identity@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-2.3.0.tgz#6b01a210c9485355c2a913b48b6711219a173ded" +lodash.intersection@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.intersection/-/lodash.intersection-4.4.0.tgz#0a11ba631d0e95c23c7f2f4cbb9a692ed178e705" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" From d4ab1a23878cd3557a26cf41dd5881f1ad4f0784 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 9 May 2018 17:09:07 -0700 Subject: [PATCH 09/17] Show a warning on task rows on the alloc detail page The warning shows up when the task's driver is unhealthy on the node the task is running on. --- ui/app/templates/allocations/allocation/index.hbs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 1aa22ab0a..b95afd44c 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -40,6 +40,7 @@ sortDescending=sortDescending class="is-striped" as |t|}} {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} {{#t.sort-by prop="state"}}State{{/t.sort-by}} Last Event @@ -48,6 +49,13 @@ {{/t.head}} {{#t.body as |row|}} + + {{#if (not row.model.driverStatus.healthy)}} + + {{x-icon "warning" class="is-warning"}} + + {{/if}} + {{#link-to "allocations.allocation.task" row.model.allocation row.model}} {{row.model.name}} From 6c44b6da9da67747e2c4a8e223657a36acaea5f4 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 10 May 2018 16:28:18 -0700 Subject: [PATCH 10/17] Fix narrow table column padding --- ui/app/styles/core/table.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss index 97ff41845..63822653b 100644 --- a/ui/app/styles/core/table.scss +++ b/ui/app/styles/core/table.scss @@ -86,6 +86,11 @@ &.is-narrow { padding: 1.25em 0 1.25em 0.5em; + + & + th, + & + td { + padding-left: 0.5em; + } } // Only use px modifiers when text needs to be truncated. From 7539ea739bb1722b5f8c6e231ea6ad2b708ffc56 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 May 2018 11:35:48 -0700 Subject: [PATCH 11/17] Add driver warning to allocation rows --- ui/app/templates/components/allocation-row.hbs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index b2f7e1103..8f8590702 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -1,4 +1,9 @@ + {{#if allocation.unhealthyDrivers.length}} + + {{x-icon "warning" class="is-warning"}} + + {{/if}} {{#if allocation.nextAllocation}} {{x-icon "history" class="is-faded"}} From c32738e774b15605564a71cefedea4399bdd9e2b Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 May 2018 17:30:15 -0700 Subject: [PATCH 12/17] NodeEvent and NodeDriver modeling in Mirage --- ui/mirage/factories/node-event.js | 12 ++++++++++ ui/mirage/factories/node.js | 40 +++++++++++++++++++++++++++++++ ui/mirage/factories/task-event.js | 2 +- ui/mirage/factories/task.js | 3 +++ ui/mirage/models/node.js | 5 ++++ ui/mirage/serializers/node.js | 6 +++++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 ui/mirage/factories/node-event.js create mode 100644 ui/mirage/models/node.js create mode 100644 ui/mirage/serializers/node.js diff --git a/ui/mirage/factories/node-event.js b/ui/mirage/factories/node-event.js new file mode 100644 index 000000000..a00435d56 --- /dev/null +++ b/ui/mirage/factories/node-event.js @@ -0,0 +1,12 @@ +import { Factory, faker } from 'ember-cli-mirage'; +import { provide } from '../utils'; + +const REF_TIME = new Date(); +const STATES = provide(10, faker.system.fileExt.bind(faker.system)); + +export default Factory.extend({ + subsystem: faker.list.random(...STATES), + message: () => faker.lorem.sentence(), + time: () => faker.date.past(2 / 365, REF_TIME), + details: null, +}); diff --git a/ui/mirage/factories/node.js b/ui/mirage/factories/node.js index ba4bd569e..3097ac19f 100644 --- a/ui/mirage/factories/node.js +++ b/ui/mirage/factories/node.js @@ -4,6 +4,7 @@ import { DATACENTERS, HOSTS } from '../common'; const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); const NODE_STATUSES = ['initializing', 'ready', 'down']; +const REF_DATE = new Date(); export default Factory.extend({ id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]), @@ -28,6 +29,8 @@ export default Factory.extend({ }, }), + drivers: makeDrivers, + attributes() { // TODO add variability to these return { @@ -72,5 +75,42 @@ export default Factory.extend({ server.create('client-stats', { id: node.httpAddr, }); + + const events = server.createList('node-event', faker.random.number({ min: 1, max: 10 }), { + nodeId: node.id, + }); + + node.update({ + eventIds: events.mapBy('id'), + }); }, }); + +function makeDrivers() { + const generate = () => { + const detected = Math.random() > 0.3; + const healthy = detected && Math.random() > 0.3; + const attributes = { + 'driver.name.version': '1.0.0', + 'driver.name.status': 'awesome', + 'driver.name.more.details': 'yeah', + 'driver.name.more.again': 'we got that', + }; + return { + Detected: detected, + Healthy: healthy, + HealthDescription: healthy ? 'Driver is healthy' : 'Uh oh', + UpdateTime: faker.date.past(5 / 365, REF_DATE), + Attributes: Math.random() > 0.3 && detected ? attributes : null, + }; + }; + + return { + docker: generate(), + rkt: generate(), + qemu: generate(), + exec: generate(), + raw_exec: generate(), + java: generate(), + }; +} diff --git a/ui/mirage/factories/task-event.js b/ui/mirage/factories/task-event.js index ab161cdb2..72397110c 100644 --- a/ui/mirage/factories/task-event.js +++ b/ui/mirage/factories/task-event.js @@ -1,4 +1,4 @@ -import { Factory, faker, trait } from 'ember-cli-mirage'; +import { Factory, faker } from 'ember-cli-mirage'; import { provide } from '../utils'; const REF_TIME = new Date(); diff --git a/ui/mirage/factories/task.js b/ui/mirage/factories/task.js index 47afe3bbd..49733e938 100644 --- a/ui/mirage/factories/task.js +++ b/ui/mirage/factories/task.js @@ -1,6 +1,8 @@ import { Factory, faker } from 'ember-cli-mirage'; import { generateResources } from '../common'; +const DRIVERS = ['docker', 'java', 'rkt', 'qemu', 'exec', 'raw_exec']; + export default Factory.extend({ // Hidden property used to compute the Summary hash groupNames: [], @@ -8,6 +10,7 @@ export default Factory.extend({ JobID: '', name: id => `task-${faker.hacker.noun()}-${id}`, + driver: faker.list.random(...DRIVERS), Resources: generateResources, }); diff --git a/ui/mirage/models/node.js b/ui/mirage/models/node.js new file mode 100644 index 000000000..e42622483 --- /dev/null +++ b/ui/mirage/models/node.js @@ -0,0 +1,5 @@ +import { Model, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + events: hasMany('node-event'), +}); diff --git a/ui/mirage/serializers/node.js b/ui/mirage/serializers/node.js new file mode 100644 index 000000000..506c19038 --- /dev/null +++ b/ui/mirage/serializers/node.js @@ -0,0 +1,6 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['events'], +}); From 9b45d35e8fa4be3dcbafe06bd6713919df07b89d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 12 May 2018 20:00:34 -0700 Subject: [PATCH 13/17] Disable visibility behaviors when testing It results in surprise behaviors. --- ui/app/mixins/with-component-visibility-detection.js | 11 ++++++++--- ui/app/mixins/with-route-visibility-detection.js | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ui/app/mixins/with-component-visibility-detection.js b/ui/app/mixins/with-component-visibility-detection.js index 66d1097ee..605937834 100644 --- a/ui/app/mixins/with-component-visibility-detection.js +++ b/ui/app/mixins/with-component-visibility-detection.js @@ -1,3 +1,4 @@ +import Ember from 'ember'; import Mixin from '@ember/object/mixin'; import { assert } from '@ember/debug'; @@ -7,11 +8,15 @@ export default Mixin.create({ }, setupDocumentVisibility: function() { - this.set('_visibilityHandler', this.get('visibilityHandler').bind(this)); - document.addEventListener('visibilitychange', this.get('_visibilityHandler')); + if (!Ember.testing) { + this.set('_visibilityHandler', this.get('visibilityHandler').bind(this)); + document.addEventListener('visibilitychange', this.get('_visibilityHandler')); + } }.on('init'), removeDocumentVisibility: function() { - document.removeEventListener('visibilitychange', this.get('_visibilityHandler')); + if (!Ember.testing) { + document.removeEventListener('visibilitychange', this.get('_visibilityHandler')); + } }.on('willDestroy'), }); diff --git a/ui/app/mixins/with-route-visibility-detection.js b/ui/app/mixins/with-route-visibility-detection.js index 1df6e5f45..57e8300e3 100644 --- a/ui/app/mixins/with-route-visibility-detection.js +++ b/ui/app/mixins/with-route-visibility-detection.js @@ -1,3 +1,4 @@ +import Ember from 'ember'; import Mixin from '@ember/object/mixin'; import { assert } from '@ember/debug'; @@ -7,11 +8,15 @@ export default Mixin.create({ }, setupDocumentVisibility: function() { - this.set('_visibilityHandler', this.get('visibilityHandler').bind(this)); - document.addEventListener('visibilitychange', this.get('_visibilityHandler')); + if (!Ember.testing) { + this.set('_visibilityHandler', this.get('visibilityHandler').bind(this)); + document.addEventListener('visibilitychange', this.get('_visibilityHandler')); + } }.on('activate'), removeDocumentVisibility: function() { - document.removeEventListener('visibilitychange', this.get('_visibilityHandler')); + if (!Ember.testing) { + document.removeEventListener('visibilitychange', this.get('_visibilityHandler')); + } }.on('deactivate'), }); From 37a235c2af3b77b17e3adb594d82f294d1d80e70 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 12 May 2018 20:01:51 -0700 Subject: [PATCH 14/17] Acceptance tests for node driver and node events --- .../list-accordion/accordion-head.js | 2 + .../allocations/allocation/index.hbs | 2 +- ui/app/templates/clients/client.hbs | 24 +-- .../templates/components/allocation-row.hbs | 4 +- .../list-accordion/accordion-body.hbs | 2 +- .../list-accordion/accordion-head.hbs | 1 + ui/tests/acceptance/allocation-detail-test.js | 22 +- ui/tests/acceptance/client-detail-test.js | 193 ++++++++++++++++-- ui/tests/acceptance/task-group-detail-test.js | 9 +- ui/tests/integration/allocation-row-test.js | 40 ++++ ui/tests/unit/adapters/node-test.js | 4 + ui/tests/unit/serializers/node-test.js | 8 +- 12 files changed, 278 insertions(+), 33 deletions(-) diff --git a/ui/app/components/list-accordion/accordion-head.js b/ui/app/components/list-accordion/accordion-head.js index 4de6d4edc..9cff6e92b 100644 --- a/ui/app/components/list-accordion/accordion-head.js +++ b/ui/app/components/list-accordion/accordion-head.js @@ -4,6 +4,8 @@ export default Component.extend({ classNames: ['accordion-head'], classNameBindings: ['isOpen::is-light', 'isExpandable::is-inactive'], + 'data-test-accordion-head': true, + buttonLabel: 'toggle', isOpen: false, isExpandable: true, diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index b95afd44c..c2e0a60ce 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -51,7 +51,7 @@ {{#if (not row.model.driverStatus.healthy)}} - + {{x-icon "warning" class="is-warning"}} {{/if}} diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 79a18e34c..00da3de68 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -93,7 +93,7 @@
-
+
Client Events
@@ -105,10 +105,10 @@ Message {{/t.head}} {{#t.body as |row|}} - - {{moment-format row.model.time "MM/DD/YY HH:mm:ss"}} - {{row.model.subsystem}} - + + {{moment-format row.model.time "MM/DD/YY HH:mm:ss"}} + {{row.model.subsystem}} + {{#if row.model.message}} {{row.model.message}} {{else}} @@ -121,7 +121,7 @@
-
+
Driver Status
@@ -130,11 +130,11 @@ {{#a.head buttonLabel="details" isExpandable=a.item.detected}}
- {{a.item.name}} + {{a.item.name}}
{{#if a.item.detected}} - + {{if a.item.healthy "Healthy" "Unhealthy"}} @@ -143,20 +143,20 @@
Detected - {{if a.item.detected "Yes" "No"}} + {{if a.item.detected "Yes" "No"}} Last Updated - {{moment-from-now a.item.updateTime interval=1000}} + {{moment-from-now a.item.updateTime interval=1000}}
{{/a.head}} {{#a.body}} -

{{a.item.healthDescription}}

-
+

{{a.item.healthDescription}}

+
{{capitalize a.item.name}} Attributes
diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index 8f8590702..1da565ff5 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -1,11 +1,11 @@ {{#if allocation.unhealthyDrivers.length}} - + {{x-icon "warning" class="is-warning"}} {{/if}} {{#if allocation.nextAllocation}} - + {{x-icon "history" class="is-faded"}} {{/if}} diff --git a/ui/app/templates/components/list-accordion/accordion-body.hbs b/ui/app/templates/components/list-accordion/accordion-body.hbs index 894e66818..35512b2b7 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 isOpen}} -
+
{{yield}}
{{/if}} diff --git a/ui/app/templates/components/list-accordion/accordion-head.hbs b/ui/app/templates/components/list-accordion/accordion-head.hbs index a362b1165..5d5cab568 100644 --- a/ui/app/templates/components/list-accordion/accordion-head.hbs +++ b/ui/app/templates/components/list-accordion/accordion-head.hbs @@ -2,6 +2,7 @@ {{yield}}