From 1f95a674f30a9c327061fb336e1c0b9de42f95b4 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Tue, 24 Aug 2021 07:34:07 -0400 Subject: [PATCH 01/58] initial logic for computing client status bar --- ui/app/components/client-status-bar.js | 80 +++++++++++++++++++ ui/app/controllers/jobs/job/index.js | 24 ++++++ ui/app/routes/jobs/job/index.js | 9 +++ ui/app/styles/charts/colors.scss | 18 +++++ .../components/job-page/parts/summary.hbs | 58 ++++++++++++++ .../templates/components/job-page/service.hbs | 2 +- .../templates/components/job-page/system.hbs | 2 +- ui/app/templates/jobs/job/index.hbs | 2 + 8 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 ui/app/components/client-status-bar.js diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js new file mode 100644 index 000000000000..08c3d40cd2bd --- /dev/null +++ b/ui/app/components/client-status-bar.js @@ -0,0 +1,80 @@ +import { computed } from '@ember/object'; +import DistributionBar from './distribution-bar'; +import classic from 'ember-classic-decorator'; +import { countBy } from 'lodash'; + +@classic +export default class ClientStatusBar extends DistributionBar { + layoutName = 'components/distribution-bar'; + + 'data-test-client-status-bar' = true; + allocationContainer = null; + nodes = null; + totalNodes = null; + + @computed('nodes') + get data() { + const statuses = { + queued: 0, + 'not scheduled': this.totalNodes - this.nodes.length, + starting: 0, + running: 0, + complete: 0, + degraded: 0, + failed: 0, + lost: 0, + }; + for (const node of this.nodes) { + const concatenatedAllocationStatuses = [].concat(...Object.values(node)); + console.log(concatenatedAllocationStatuses); + // there is a bug that counts nodes multiple times in this part of the loop + for (const status of concatenatedAllocationStatuses) { + const val = str => str; + const statusCount = countBy(concatenatedAllocationStatuses, val); + if (Object.keys(statusCount).length === 1) { + if (statusCount.running > 0) { + statuses.running++; + } + if (statusCount.failed > 0) { + statuses.failed++; + } + if (statusCount.lost > 0) { + statuses.lost++; + } + if (statusCount.complete > 0) { + statuses.complete++; + } + } else if (Object.keys(statusCount).length !== 1 && !!statusCount.running) { + if (!!statusCount.failed || !!statusCount.lost) { + statuses.degraded++; + } + } else if (Object.keys(statusCount).length !== 1 && !!statusCount.pending) { + statuses.starting++; + } else { + statuses.queued++; + } + } + } + + console.log('statuses\n\n', statuses); + return [ + { label: 'Not Scheduled', value: statuses['not scheduled'], className: 'not-scheduled' }, + { label: 'Queued', value: statuses.queued, className: 'queued' }, + { + label: 'Starting', + value: statuses.starting, + className: 'starting', + layers: 2, + }, + { label: 'Running', value: statuses.running, className: 'running' }, + { + label: 'Complete', + value: statuses.complete, + className: 'complete', + }, + { label: 'Degraded', value: statuses.degraded, className: 'degraded' }, + { label: 'Failed', value: statuses.failed, className: 'failed' }, + { label: 'Lost', value: statuses.lost, className: 'lost' }, + ]; + } +} diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index eae91858f7a6..18a9b685d658 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -1,5 +1,6 @@ import { inject as service } from '@ember/service'; import { alias } from '@ember/object/computed'; +import { computed } from '@ember/object'; import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { action } from '@ember/object'; @@ -8,6 +9,29 @@ import classic from 'ember-classic-decorator'; @classic export default class IndexController extends Controller.extend(WithNamespaceResetting) { @service system; + @service store; + + @computed('job') + get uniqueNodes() { + // add datacenter filter + const allocs = this.job.allocations; + const nodes = allocs.mapBy('node'); + const uniqueNodes = nodes.uniqBy('id').toArray(); + return uniqueNodes.map(nodeId => { + return { + [nodeId.get('id')]: allocs + .toArray() + .filter(alloc => nodeId.get('id') === alloc.get('node.id')) + .map(alloc => alloc.getProperties('clientStatus')) + .map(alloc => alloc.clientStatus), + }; + }); + } + + @computed('node') + get totalNodes() { + return this.store.peekAll('node').toArray().length; + } queryParams = [ { diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index 4bea45f1f2d7..933fcf95ba95 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -4,6 +4,12 @@ import { watchRecord, watchRelationship, watchAll } from 'nomad-ui/utils/propert import WithWatchers from 'nomad-ui/mixins/with-watchers'; export default class IndexRoute extends Route.extend(WithWatchers) { + async model() { + // Optimizing future node look ups by preemptively loading everything + await this.store.findAll('node'); + return this.modelFor('jobs.job'); + } + startWatchers(controller, model) { if (!model) { return; @@ -16,6 +22,7 @@ export default class IndexRoute extends Route.extend(WithWatchers) { latestDeployment: model.get('supportsDeployments') && this.watchLatestDeployment.perform(model), list: model.get('hasChildren') && this.watchAll.perform(), + nodes: /*model.type === 'sysbatch' && */ this.watchNodes.perform(), }); } @@ -33,6 +40,7 @@ export default class IndexRoute extends Route.extend(WithWatchers) { @watchRecord('job') watch; @watchAll('job') watchAll; + @watchAll('node') watchNodes; @watchRecord('job-summary') watchSummary; @watchRelationship('allocations') watchAllocations; @watchRelationship('evaluations') watchEvaluations; @@ -41,6 +49,7 @@ export default class IndexRoute extends Route.extend(WithWatchers) { @collect( 'watch', 'watchAll', + 'watchNodes', 'watchSummary', 'watchAllocations', 'watchEvaluations', diff --git a/ui/app/styles/charts/colors.scss b/ui/app/styles/charts/colors.scss index 50f370607f67..252922061776 100644 --- a/ui/app/styles/charts/colors.scss +++ b/ui/app/styles/charts/colors.scss @@ -4,6 +4,8 @@ $running: $primary; $complete: $nomad-green-dark; $failed: $danger; $lost: $dark; +$not-scheduled: $grey-darker; +$degraded: $warning; .chart { .queued { @@ -37,6 +39,14 @@ $lost: $dark; .lost { fill: $lost; } + + .not-scheduled { + fill: $not-scheduled; + } + + .degraded { + fill: $degraded; + } } .color-swatch { @@ -102,6 +112,14 @@ $lost: $dark; background: $lost; } + &.not-scheduled { + fill: $not-scheduled; + } + + &.degraded { + fill: $degraded; + } + @each $name, $pair in $colors { $color: nth($pair, 1); diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index e90cdff53753..11130804d604 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -1,4 +1,62 @@ + +
+
+ {{#if a.item.hasChildren}} + Children Status + + {{a.item.summary.totalChildren}} + + {{else}} + Client Status + + {{a.item.summary.totalAllocs}} + + {{/if}} +
+ + {{#unless a.isOpen}} +
+
+ {{#if a.item.hasChildren}} + {{#if (gt a.item.totalChildren 0)}} + + {{else}} + No Children + {{/if}} + {{else}} + + {{/if}} +
+
+ {{/unless}} +
+
+ + {{#component (if a.item.hasChildren "children-status-bar" "client-status-bar") + allocationContainer=a.item.summary + job=a.item.summary + nodes=this.nodes + totalNodes=this.totalNodes + 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/service.hbs b/ui/app/templates/components/job-page/service.hbs index d8180f244a8c..915e88bab962 100644 --- a/ui/app/templates/components/job-page/service.hbs +++ b/ui/app/templates/components/job-page/service.hbs @@ -19,7 +19,7 @@ {{/each}} {{/if}} - + diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 5e0abbea25f7..a7c8520b2ae6 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -19,7 +19,7 @@ {{/each}} {{/if}} - + diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index 56d18ec2e57c..82d97770fbe1 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -1,6 +1,8 @@ {{page-title "Job " this.model.name}} {{component (concat "job-page/" this.model.templateType) job=this.model + nodes=this.uniqueNodes + totalNodes=this.totalNodes sortProperty=this.sortProperty sortDescending=this.sortDescending currentPage=this.currentPage From c4584e7f186adc43737f1bca8ebbe3e554c2a576 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Tue, 24 Aug 2021 11:17:41 -0400 Subject: [PATCH 02/58] edit the schema of uniqueNodes returned by the controller --- ui/app/components/client-status-bar.js | 57 +++++++++++++------------- ui/app/controllers/jobs/job/index.js | 7 ++-- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js index 08c3d40cd2bd..c536381afea6 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/client-status-bar.js @@ -24,39 +24,40 @@ export default class ClientStatusBar extends DistributionBar { failed: 0, lost: 0, }; - for (const node of this.nodes) { - const concatenatedAllocationStatuses = [].concat(...Object.values(node)); - console.log(concatenatedAllocationStatuses); - // there is a bug that counts nodes multiple times in this part of the loop - for (const status of concatenatedAllocationStatuses) { - const val = str => str; - const statusCount = countBy(concatenatedAllocationStatuses, val); - if (Object.keys(statusCount).length === 1) { - if (statusCount.running > 0) { - statuses.running++; - } - if (statusCount.failed > 0) { - statuses.failed++; - } - if (statusCount.lost > 0) { - statuses.lost++; - } - if (statusCount.complete > 0) { - statuses.complete++; - } - } else if (Object.keys(statusCount).length !== 1 && !!statusCount.running) { - if (!!statusCount.failed || !!statusCount.lost) { - statuses.degraded++; - } - } else if (Object.keys(statusCount).length !== 1 && !!statusCount.pending) { + const formattedNodes = this.nodes.map(node => { + const [[_, allocs]] = Object.entries(node); + return allocs.map(alloc => alloc.clientStatus); + }); + for (const node of formattedNodes) { + const statusCount = countBy(node, status => status); + const hasOnly1Status = Object.keys(statusCount).length === 1; + + if (hasOnly1Status) { + if (statusCount.running > 0) { + statuses.running++; + } + if (statusCount.failed > 0) { + statuses.failed++; + } + if (statusCount.lost > 0) { + statuses.lost++; + } + if (statusCount.complete > 0) { + statuses.complete++; + } + } else if (!hasOnly1Status && !!statusCount.running) { + if (!!statusCount.failed || !!statusCount.lost) { + statuses.degraded++; + } else if (statusCount.pending) { statuses.starting++; - } else { - statuses.queued++; } + } else { + // if no allocations then queued -- job registered, hasn't been assigned clients to run -- no allocations + // may only have this state for a few milliseconds + statuses.queued++; } } - console.log('statuses\n\n', statuses); return [ { label: 'Not Scheduled', value: statuses['not scheduled'], className: 'not-scheduled' }, { label: 'Queued', value: statuses.queued, className: 'queued' }, diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 18a9b685d658..5129445b0622 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -17,15 +17,16 @@ export default class IndexController extends Controller.extend(WithNamespaceRese const allocs = this.job.allocations; const nodes = allocs.mapBy('node'); const uniqueNodes = nodes.uniqBy('id').toArray(); - return uniqueNodes.map(nodeId => { + const result = uniqueNodes.map(nodeId => { return { [nodeId.get('id')]: allocs .toArray() .filter(alloc => nodeId.get('id') === alloc.get('node.id')) - .map(alloc => alloc.getProperties('clientStatus')) - .map(alloc => alloc.clientStatus), + .map(alloc => alloc.getProperties('clientStatus', 'name', 'createTime', 'modifyTime')), }; }); + console.log('result\n\n', result); + return result; } @computed('node') From 38f764fecee0a10b059c19fd74f70e991744c865 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 24 Aug 2021 15:36:17 -0400 Subject: [PATCH 03/58] refactoring job client status --- ui/app/controllers/jobs/job/index.js | 7 ++++++ .../components/job-page/parts/summary.hbs | 4 ++-- .../templates/components/job-page/service.hbs | 2 +- ui/app/templates/jobs/job/index.hbs | 1 + ui/app/utils/properties/job-client-status.js | 23 +++++++++++++++++++ ui/mirage/factories/job.js | 2 +- ui/mirage/scenarios/default.js | 2 ++ ui/mirage/scenarios/sysbatch.js | 18 +++++++++++++++ ui/package.json | 1 + ui/yarn.lock | 2 +- 10 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 ui/app/utils/properties/job-client-status.js create mode 100644 ui/mirage/scenarios/sysbatch.js diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 5129445b0622..25b37f4aeeb5 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -5,12 +5,15 @@ import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { action } from '@ember/object'; import classic from 'ember-classic-decorator'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic export default class IndexController extends Controller.extend(WithNamespaceResetting) { @service system; @service store; + @jobClientStatus('nodes', 'job.status', 'job.allocations') jobClientStatus; + @computed('job') get uniqueNodes() { // add datacenter filter @@ -34,6 +37,10 @@ export default class IndexController extends Controller.extend(WithNamespaceRese return this.store.peekAll('node').toArray().length; } + get nodes() { + return this.store.peekAll('node'); + } + queryParams = [ { currentPage: 'page', diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index 11130804d604..fae0669c7004 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -25,7 +25,7 @@ No Children {{/if}} {{else}} - + {{/if}}
@@ -38,6 +38,7 @@ job=a.item.summary nodes=this.nodes totalNodes=this.totalNodes + jobClientStatus=this.jobClientStatus class="split-view" as |chart|}}
    {{#each chart.data as |datum index|}} @@ -109,4 +110,3 @@ {{/component}} - diff --git a/ui/app/templates/components/job-page/service.hbs b/ui/app/templates/components/job-page/service.hbs index 915e88bab962..0e06c5a937e8 100644 --- a/ui/app/templates/components/job-page/service.hbs +++ b/ui/app/templates/components/job-page/service.hbs @@ -19,7 +19,7 @@ {{/each}} {{/if}} - + diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index 82d97770fbe1..e1b7bc80172c 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -3,6 +3,7 @@ job=this.model nodes=this.uniqueNodes totalNodes=this.totalNodes + jobClientStatus=this.jobClientStatus sortProperty=this.sortProperty sortDescending=this.sortDescending currentPage=this.currentPage diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js new file mode 100644 index 000000000000..f050335489dd --- /dev/null +++ b/ui/app/utils/properties/job-client-status.js @@ -0,0 +1,23 @@ +import { computed } from '@ember/object'; + +// An Ember.Computed property that persists set values in localStorage +// and will attempt to get its initial value from localStorage before +// falling back to a default. +// +// ex. showTutorial: localStorageProperty('nomadTutorial', true), +export default function jobClientStatus(nodesKey, jobStatusKey, jobAllocsKey) { + return computed(nodesKey, jobStatusKey, jobAllocsKey, function() { + const allocs = this.get(jobAllocsKey); + const jobStatus = this.get(jobStatusKey); + const nodes = this.get(nodesKey); + + return { + byNode: { + '123': 'running', + }, + byStatus: { + running: ['123'], + }, + }; + }); +} diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index 1c914c7a991e..ce57711580b4 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -6,7 +6,7 @@ import { DATACENTERS } from '../common'; const REF_TIME = new Date(); const JOB_PREFIXES = provide(5, faker.hacker.abbreviation); -const JOB_TYPES = ['service', 'batch', 'system']; +const JOB_TYPES = ['service', 'batch', 'system', 'sysbatch']; const JOB_STATUSES = ['pending', 'running', 'dead']; export default Factory.extend({ diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js index 8d2423f86d54..1128b7a884aa 100644 --- a/ui/mirage/scenarios/default.js +++ b/ui/mirage/scenarios/default.js @@ -1,5 +1,6 @@ import config from 'nomad-ui/config/environment'; import * as topoScenarios from './topo'; +import * as sysbatchScenarios from './sysbatch'; import { pickOne } from '../utils'; const withNamespaces = getConfigValue('mirageWithNamespaces', false); @@ -16,6 +17,7 @@ const allScenarios = { everyFeature, emptyCluster, ...topoScenarios, + ...sysbatchScenarios, }; const scenario = getScenarioQueryParameter() || getConfigValue('mirageScenario', 'emptyCluster'); diff --git a/ui/mirage/scenarios/sysbatch.js b/ui/mirage/scenarios/sysbatch.js new file mode 100644 index 000000000000..7ffae26a35e2 --- /dev/null +++ b/ui/mirage/scenarios/sysbatch.js @@ -0,0 +1,18 @@ +export function sysbatchSmall(server) { + server.createList('agent', 3); + server.createList('node', 12, { + datacenter: 'dc1', + status: 'ready', + }); + + const jobConstraints = [[], [], [], [], [], []]; + + jobConstraints.forEach(spec => { + server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'sysbatch', + createAllocations: true, + }); + }); +} diff --git a/ui/package.json b/ui/package.json index 3fac4031abef..6ef08394a531 100644 --- a/ui/package.json +++ b/ui/package.json @@ -153,6 +153,7 @@ ] }, "dependencies": { + "lodash": "^4.17.21", "lru_map": "^0.3.3", "no-case": "^3.0.4", "title-case": "^3.0.3" diff --git a/ui/yarn.lock b/ui/yarn.lock index d8fbfad2444e..3d0702ef6b77 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -11746,7 +11746,7 @@ lodash.values@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c= -lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.1: +lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== From 29b107b5abdbbee11ef18f9b1c2a968c26dde874 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 24 Aug 2021 19:31:22 -0400 Subject: [PATCH 04/58] first implementation of job client status --- ui/app/components/client-status-bar.js | 92 ++++++-------- ui/app/controllers/jobs/job/index.js | 27 +--- ui/app/styles/charts/colors.scss | 6 +- .../components/job-page/parts/summary.hbs | 6 +- .../templates/components/job-page/service.hbs | 2 +- .../templates/components/job-page/system.hbs | 2 +- ui/app/templates/jobs/job/index.hbs | 2 - ui/app/utils/properties/job-client-status.js | 115 ++++++++++++++++-- ui/mirage/scenarios/sysbatch.js | 59 +++++++-- ui/package.json | 1 - ui/yarn.lock | 2 +- 11 files changed, 195 insertions(+), 119 deletions(-) diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js index c536381afea6..394cdd74760b 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/client-status-bar.js @@ -1,81 +1,57 @@ import { computed } from '@ember/object'; import DistributionBar from './distribution-bar'; import classic from 'ember-classic-decorator'; -import { countBy } from 'lodash'; @classic export default class ClientStatusBar extends DistributionBar { layoutName = 'components/distribution-bar'; 'data-test-client-status-bar' = true; - allocationContainer = null; - nodes = null; - totalNodes = null; + jobClientStatus = null; - @computed('nodes') + @computed('jobClientStatus') get data() { - const statuses = { - queued: 0, - 'not scheduled': this.totalNodes - this.nodes.length, - starting: 0, - running: 0, - complete: 0, - degraded: 0, - failed: 0, - lost: 0, - }; - const formattedNodes = this.nodes.map(node => { - const [[_, allocs]] = Object.entries(node); - return allocs.map(alloc => alloc.clientStatus); - }); - for (const node of formattedNodes) { - const statusCount = countBy(node, status => status); - const hasOnly1Status = Object.keys(statusCount).length === 1; - - if (hasOnly1Status) { - if (statusCount.running > 0) { - statuses.running++; - } - if (statusCount.failed > 0) { - statuses.failed++; - } - if (statusCount.lost > 0) { - statuses.lost++; - } - if (statusCount.complete > 0) { - statuses.complete++; - } - } else if (!hasOnly1Status && !!statusCount.running) { - if (!!statusCount.failed || !!statusCount.lost) { - statuses.degraded++; - } else if (statusCount.pending) { - statuses.starting++; - } - } else { - // if no allocations then queued -- job registered, hasn't been assigned clients to run -- no allocations - // may only have this state for a few milliseconds - statuses.queued++; - } - } - return [ - { label: 'Not Scheduled', value: statuses['not scheduled'], className: 'not-scheduled' }, - { label: 'Queued', value: statuses.queued, className: 'queued' }, + { + label: 'Queued', + value: this.jobClientStatus.byStatus.queued.length, + className: 'queued', + }, { label: 'Starting', - value: statuses.starting, + value: this.jobClientStatus.byStatus.starting.length, className: 'starting', - layers: 2, }, - { label: 'Running', value: statuses.running, className: 'running' }, + { + label: 'Running', + value: this.jobClientStatus.byStatus.running.length, + className: 'running', + }, { label: 'Complete', - value: statuses.complete, + value: this.jobClientStatus.byStatus.complete.length, className: 'complete', }, - { label: 'Degraded', value: statuses.degraded, className: 'degraded' }, - { label: 'Failed', value: statuses.failed, className: 'failed' }, - { label: 'Lost', value: statuses.lost, className: 'lost' }, + { + label: 'Degraded', + value: this.jobClientStatus.byStatus.degraded.length, + className: 'degraded', + }, + { + label: 'Failed', + value: this.jobClientStatus.byStatus.failed.length, + className: 'failed', + }, + { + label: 'Lost', + value: this.jobClientStatus.byStatus.lost.length, + className: 'lost', + }, + { + label: 'Not Scheduled', + value: this.jobClientStatus.byStatus.notScheduled.length, + className: 'not-scheduled', + }, ]; } } diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 25b37f4aeeb5..d59a8bf0ae31 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -1,6 +1,5 @@ import { inject as service } from '@ember/service'; import { alias } from '@ember/object/computed'; -import { computed } from '@ember/object'; import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { action } from '@ember/object'; @@ -12,31 +11,9 @@ export default class IndexController extends Controller.extend(WithNamespaceRese @service system; @service store; - @jobClientStatus('nodes', 'job.status', 'job.allocations') jobClientStatus; - - @computed('job') - get uniqueNodes() { - // add datacenter filter - const allocs = this.job.allocations; - const nodes = allocs.mapBy('node'); - const uniqueNodes = nodes.uniqBy('id').toArray(); - const result = uniqueNodes.map(nodeId => { - return { - [nodeId.get('id')]: allocs - .toArray() - .filter(alloc => nodeId.get('id') === alloc.get('node.id')) - .map(alloc => alloc.getProperties('clientStatus', 'name', 'createTime', 'modifyTime')), - }; - }); - console.log('result\n\n', result); - return result; - } - - @computed('node') - get totalNodes() { - return this.store.peekAll('node').toArray().length; - } + @jobClientStatus('nodes', 'job') jobClientStatus; + // TODO: use watch get nodes() { return this.store.peekAll('node'); } diff --git a/ui/app/styles/charts/colors.scss b/ui/app/styles/charts/colors.scss index 252922061776..1676fdc8937d 100644 --- a/ui/app/styles/charts/colors.scss +++ b/ui/app/styles/charts/colors.scss @@ -4,7 +4,7 @@ $running: $primary; $complete: $nomad-green-dark; $failed: $danger; $lost: $dark; -$not-scheduled: $grey-darker; +$not-scheduled: $blue-200; $degraded: $warning; .chart { @@ -113,11 +113,11 @@ $degraded: $warning; } &.not-scheduled { - fill: $not-scheduled; + background: $not-scheduled; } &.degraded { - fill: $degraded; + background: $degraded; } @each $name, $pair in $colors { diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index fae0669c7004..1405a3fe9f9a 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -25,7 +25,7 @@ No Children {{/if}} {{else}} - + {{/if}} @@ -34,10 +34,6 @@ {{#component (if a.item.hasChildren "children-status-bar" "client-status-bar") - allocationContainer=a.item.summary - job=a.item.summary - nodes=this.nodes - totalNodes=this.totalNodes jobClientStatus=this.jobClientStatus class="split-view" as |chart|}}
      diff --git a/ui/app/templates/components/job-page/service.hbs b/ui/app/templates/components/job-page/service.hbs index 0e06c5a937e8..c80e664d3450 100644 --- a/ui/app/templates/components/job-page/service.hbs +++ b/ui/app/templates/components/job-page/service.hbs @@ -19,7 +19,7 @@ {{/each}} {{/if}} - + diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index a7c8520b2ae6..4b97c0067c87 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -19,7 +19,7 @@ {{/each}} {{/if}} - + diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index e1b7bc80172c..ea009c855b5b 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -1,8 +1,6 @@ {{page-title "Job " this.model.name}} {{component (concat "job-page/" this.model.templateType) job=this.model - nodes=this.uniqueNodes - totalNodes=this.totalNodes jobClientStatus=this.jobClientStatus sortProperty=this.sortProperty sortDescending=this.sortDescending diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index f050335489dd..eb7f9e61e89f 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -5,19 +5,108 @@ import { computed } from '@ember/object'; // falling back to a default. // // ex. showTutorial: localStorageProperty('nomadTutorial', true), -export default function jobClientStatus(nodesKey, jobStatusKey, jobAllocsKey) { - return computed(nodesKey, jobStatusKey, jobAllocsKey, function() { - const allocs = this.get(jobAllocsKey); - const jobStatus = this.get(jobStatusKey); - const nodes = this.get(nodesKey); - - return { - byNode: { - '123': 'running', - }, - byStatus: { - running: ['123'], - }, + +const STATUS = [ + 'queued', + 'notScheduled', + 'starting', + 'running', + 'complete', + 'partial', + 'degraded', + 'failed', + 'lost', +]; + +export default function jobClientStatus(nodesKey, jobKey) { + return computed(nodesKey, jobKey, function() { + const job = this.get(jobKey); + const nodes = this.get(nodesKey).filter(n => { + return job.datacenters.indexOf(n.datacenter) >= 0; + }); + + if (job.status === 'pending') { + return allQueued(nodes); + } + + const allocsByNodeID = {}; + job.allocations.forEach(a => { + const nodeId = a.node.get('id'); + if (!(nodeId in allocsByNodeID)) { + allocsByNodeID[nodeId] = []; + } + allocsByNodeID[nodeId].push(a); + }); + + const result = { + byNode: {}, + byStatus: {}, }; + nodes.forEach(n => { + const status = jobStatus(allocsByNodeID[n.id], job.taskGroups.length); + result.byNode[n.id] = status; + + if (!(status in result.byStatus)) { + result.byStatus[status] = []; + } + result.byStatus[status].push(n.id); + }); + result.byStatus = canonicalizeStatus(result.byStatus); + return result; }); } + +function allQueued(nodes) { + const nodeIDs = nodes.map(n => n.id); + return { + byNode: Object.fromEntries(nodeIDs.map(id => [id, 'queued'])), + byStatus: canonicalizeStatus({ queued: nodeIDs }), + }; +} + +function canonicalizeStatus(status) { + for (let i = 0; i < STATUS.length; i++) { + const s = STATUS[i]; + if (!(s in status)) { + status[s] = []; + } + } + return status; +} + +function jobStatus(allocs, expected) { + if (!allocs) { + return 'notScheduled'; + } + + if (allocs.length < expected) { + return 'partial'; + } + + const summary = allocs.reduce((acc, a) => { + const status = a.clientStatus; + if (!(status in acc)) { + acc[status] = 0; + } + acc[status]++; + return acc; + }, {}); + + const terminalStatus = ['failed', 'lost', 'complete']; + for (let i = 0; i < terminalStatus.length; i++) { + const s = terminalStatus[i]; + if (summary[s] === expected) { + return s; + } + } + + if (summary['failed'] > 0 || summary['lost'] > 0) { + return 'degraded'; + } + + if (summary['running'] > 0) { + return 'running'; + } + + return 'starting'; +} diff --git a/ui/mirage/scenarios/sysbatch.js b/ui/mirage/scenarios/sysbatch.js index 7ffae26a35e2..7689138e734d 100644 --- a/ui/mirage/scenarios/sysbatch.js +++ b/ui/mirage/scenarios/sysbatch.js @@ -1,18 +1,59 @@ export function sysbatchSmall(server) { server.createList('agent', 3); - server.createList('node', 12, { + const clients = server.createList('node', 12, { datacenter: 'dc1', status: 'ready', }); - const jobConstraints = [[], [], [], [], [], []]; + // Job with 1 task group. + const job1 = server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'sysbatch', + resourceSpec: ['M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach(c => { + server.create('allocation', { jobId: job1.id, nodeId: c.id }); + }); + + // Job with 2 task groups. + const job2 = server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'sysbatch', + resourceSpec: ['M: 256, C: 500', 'M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach(c => { + server.create('allocation', { jobId: job2.id, nodeId: c.id }); + server.create('allocation', { jobId: job2.id, nodeId: c.id }); + }); - jobConstraints.forEach(spec => { - server.create('job', { - status: 'running', - datacenters: ['dc1'], - type: 'sysbatch', - createAllocations: true, - }); + // Job with 3 task groups. + const job3 = server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'sysbatch', + resourceSpec: ['M: 256, C: 500', 'M: 256, C: 500', 'M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach(c => { + server.create('allocation', { jobId: job3.id, nodeId: c.id }); + server.create('allocation', { jobId: job3.id, nodeId: c.id }); + server.create('allocation', { jobId: job3.id, nodeId: c.id }); + }); + + // Job with client not scheduled. + const jobNotScheduled = server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'sysbatch', + resourceSpec: ['M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach((c, i) => { + if (i > 9) return; + server.create('allocation', { jobId: jobNotScheduled.id, nodeId: c.id }); }); } diff --git a/ui/package.json b/ui/package.json index 6ef08394a531..3fac4031abef 100644 --- a/ui/package.json +++ b/ui/package.json @@ -153,7 +153,6 @@ ] }, "dependencies": { - "lodash": "^4.17.21", "lru_map": "^0.3.3", "no-case": "^3.0.4", "title-case": "^3.0.3" diff --git a/ui/yarn.lock b/ui/yarn.lock index 3d0702ef6b77..d8fbfad2444e 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -11746,7 +11746,7 @@ lodash.values@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c= -lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.1: +lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== From 051007daf6408d4df56ebac0cae22f53f2ce2849 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 26 Aug 2021 18:35:11 -0400 Subject: [PATCH 05/58] small improvements to the jobClientStatus helper --- ui/app/components/client-status-bar.js | 1 + ui/app/utils/properties/job-client-status.js | 49 ++++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js index 394cdd74760b..65d4a2335e6a 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/client-status-bar.js @@ -21,6 +21,7 @@ export default class ClientStatusBar extends DistributionBar { label: 'Starting', value: this.jobClientStatus.byStatus.starting.length, className: 'starting', + layers: 2, }, { label: 'Running', diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index eb7f9e61e89f..043b5499c4a0 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -1,10 +1,9 @@ import { computed } from '@ember/object'; -// An Ember.Computed property that persists set values in localStorage -// and will attempt to get its initial value from localStorage before -// falling back to a default. +// An Ember.Computed property that computes the aggregated status of a job in a +// client based on the desiredStatus of each allocation placed in the client. // -// ex. showTutorial: localStorageProperty('nomadTutorial', true), +// ex. clientStaus: jobClientStatus('nodes', 'job'), const STATUS = [ 'queued', @@ -12,15 +11,16 @@ const STATUS = [ 'starting', 'running', 'complete', - 'partial', 'degraded', 'failed', 'lost', ]; export default function jobClientStatus(nodesKey, jobKey) { - return computed(nodesKey, jobKey, function() { + return computed(nodesKey, `${jobKey}.{datacenters,status,allocations,taskGroups}`, function() { const job = this.get(jobKey); + + // Filter nodes by the datacenters defined in the job. const nodes = this.get(nodesKey).filter(n => { return job.datacenters.indexOf(n.datacenter) >= 0; }); @@ -29,10 +29,11 @@ export default function jobClientStatus(nodesKey, jobKey) { return allQueued(nodes); } + // Group the job allocations by the ID of the client that is running them. const allocsByNodeID = {}; job.allocations.forEach(a => { const nodeId = a.node.get('id'); - if (!(nodeId in allocsByNodeID)) { + if (!allocsByNodeID[nodeId]) { allocsByNodeID[nodeId] = []; } allocsByNodeID[nodeId].push(a); @@ -46,7 +47,7 @@ export default function jobClientStatus(nodesKey, jobKey) { const status = jobStatus(allocsByNodeID[n.id], job.taskGroups.length); result.byNode[n.id] = status; - if (!(status in result.byStatus)) { + if (!result.byStatus[status]) { result.byStatus[status] = []; } result.byStatus[status].push(n.id); @@ -64,42 +65,62 @@ function allQueued(nodes) { }; } +// canonicalizeStatus makes sure all possible statuses are present in the final +// returned object. Statuses missing from the input will be assigned an emtpy +// array. function canonicalizeStatus(status) { for (let i = 0; i < STATUS.length; i++) { const s = STATUS[i]; - if (!(s in status)) { + if (!status[s]) { status[s] = []; } } return status; } +// jobStatus computes the aggregated status of a job in a client. +// +// `allocs` are the list of allocations for a job that are placed in a specific +// client. +// `expected` is the number of allocations the client should have. function jobStatus(allocs, expected) { + // The `pending` status has already been checked, so if at this point the + // client doesn't have any allocations we assume that it was not considered + // for scheduling for some reason. if (!allocs) { return 'notScheduled'; } + // If there are some allocations, but not how many we expected, the job is + // considered `degraded` since it did fully run in this client. if (allocs.length < expected) { - return 'partial'; + return 'degraded'; } + // Count how many allocations are in each `clientStatus` value. const summary = allocs.reduce((acc, a) => { const status = a.clientStatus; - if (!(status in acc)) { + if (!acc[status]) { acc[status] = 0; } acc[status]++; return acc; }, {}); - const terminalStatus = ['failed', 'lost', 'complete']; - for (let i = 0; i < terminalStatus.length; i++) { - const s = terminalStatus[i]; + // Theses statuses are considered terminal, i.e., an allocation will never + // move from this status to another. + // If all of the expected allocations are in one of these statuses, the job + // as a whole is considered to be in the same status. + const terminalStatuses = ['failed', 'lost', 'complete']; + for (let i = 0; i < terminalStatuses.length; i++) { + const s = terminalStatuses[i]; if (summary[s] === expected) { return s; } } + // It only takes one allocation to be in one of these statuses for the + // entire job to be considered in a given status. if (summary['failed'] > 0 || summary['lost'] > 0) { return 'degraded'; } From 42c6b91464db962ec1e9c6ef689dec5a8ef2901a Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 26 Aug 2021 19:01:29 -0400 Subject: [PATCH 06/58] display the right number in the client status bar --- .../templates/components/job-page/parts/summary.hbs | 2 +- ui/app/utils/properties/job-client-status.js | 11 ++++++----- ui/config/environment.js | 3 ++- ui/mirage/scenarios/sysbatch.js | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index 1405a3fe9f9a..79324a2a238f 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -10,7 +10,7 @@ {{else}} Client Status - {{a.item.summary.totalAllocs}} + {{this.jobClientStatus.totalNodes}} {{/if}} diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index 043b5499c4a0..4ec57ecbf3fe 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -1,10 +1,5 @@ import { computed } from '@ember/object'; -// An Ember.Computed property that computes the aggregated status of a job in a -// client based on the desiredStatus of each allocation placed in the client. -// -// ex. clientStaus: jobClientStatus('nodes', 'job'), - const STATUS = [ 'queued', 'notScheduled', @@ -16,6 +11,10 @@ const STATUS = [ 'lost', ]; +// An Ember.Computed property that computes the aggregated status of a job in a +// client based on the desiredStatus of each allocation placed in the client. +// +// ex. clientStaus: jobClientStatus('nodes', 'job'), export default function jobClientStatus(nodesKey, jobKey) { return computed(nodesKey, `${jobKey}.{datacenters,status,allocations,taskGroups}`, function() { const job = this.get(jobKey); @@ -42,6 +41,7 @@ export default function jobClientStatus(nodesKey, jobKey) { const result = { byNode: {}, byStatus: {}, + totalNodes: nodes.length, }; nodes.forEach(n => { const status = jobStatus(allocsByNodeID[n.id], job.taskGroups.length); @@ -62,6 +62,7 @@ function allQueued(nodes) { return { byNode: Object.fromEntries(nodeIDs.map(id => [id, 'queued'])), byStatus: canonicalizeStatus({ queued: nodeIDs }), + totalNodes: nodes.length, }; } diff --git a/ui/config/environment.js b/ui/config/environment.js index 35f112b9b503..331bfdfb8a1d 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -25,7 +25,8 @@ module.exports = function(environment) { APP: { blockingQueries: true, - mirageScenario: 'topoMedium', + // TODO: revert before merging to main. + mirageScenario: 'sysbatchSmall', mirageWithNamespaces: false, mirageWithTokens: true, mirageWithRegions: true, diff --git a/ui/mirage/scenarios/sysbatch.js b/ui/mirage/scenarios/sysbatch.js index 7689138e734d..1f4c071f52aa 100644 --- a/ui/mirage/scenarios/sysbatch.js +++ b/ui/mirage/scenarios/sysbatch.js @@ -53,7 +53,7 @@ export function sysbatchSmall(server) { createAllocations: false, }); clients.forEach((c, i) => { - if (i > 9) return; + if (i > clients.length - 3) return; server.create('allocation', { jobId: jobNotScheduled.id, nodeId: c.id }); }); } From 2c96d75ccd8ef9690784cb9a96c44a831298242d Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Fri, 27 Aug 2021 10:41:41 -0400 Subject: [PATCH 07/58] create client tab view --- ui/app/components/client-row.hbs | 27 ++++++++ ui/app/components/client-row.js | 73 ++++++++++++++++++++ ui/app/controllers/jobs/job/clients.js | 67 ++++++++++++++++++ ui/app/controllers/jobs/job/index.js | 1 - ui/app/router.js | 1 + ui/app/routes/jobs/job/clients.js | 3 + ui/app/templates/components/job-subnav.hbs | 1 + ui/app/templates/jobs/job/clients.hbs | 71 +++++++++++++++++++ ui/app/utils/properties/job-client-status.js | 8 +++ 9 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 ui/app/components/client-row.hbs create mode 100644 ui/app/components/client-row.js create mode 100644 ui/app/controllers/jobs/job/clients.js create mode 100644 ui/app/routes/jobs/job/clients.js create mode 100644 ui/app/templates/jobs/job/clients.hbs diff --git a/ui/app/components/client-row.hbs b/ui/app/components/client-row.hbs new file mode 100644 index 000000000000..0b2c2defe079 --- /dev/null +++ b/ui/app/components/client-row.hbs @@ -0,0 +1,27 @@ + + + + + + {{this.id}} + + + + {{this.name}} + +{{moment-from-now this.eldestCreateTime}} + + + {{moment-from-now this.allocation.modifyTime}} + + + + {{this.status}} + + +
      + +
      + + + diff --git a/ui/app/components/client-row.js b/ui/app/components/client-row.js new file mode 100644 index 000000000000..5c84a42fb5c5 --- /dev/null +++ b/ui/app/components/client-row.js @@ -0,0 +1,73 @@ +import { classNames, tagName } from '@ember-decorators/component'; +import EmberObject from '@ember/object'; +import Component from '@glimmer/component'; + +@tagName('tr') +@classNames('client-row', 'is-interactive') +export default class ClientRowComponent extends Component { + get id() { + console.log('node arg', this.args.node); + return Object.keys(this.args.node.model)[0]; + } + + get name() { + return this.args.node.model[this.id][0].name; + } + + get eldestCreateTime() { + let eldest = null; + for (const allocation of this.args.node.model[this.id]) { + if (!eldest || allocation.createTime < eldest) { + eldest = allocation.createTime; + } + } + return eldest; + } + + get mostRecentModifyTime() { + let mostRecent = null; + for (const allocation of this.args.node.model[this.id]) { + if (!mostRecent || allocation.modifyTime > mostRecent) { + mostRecent = allocation.createTime; + } + } + return mostRecent; + } + + get status() { + return this.args.jobClientStatus.byNode[this.id]; + } + + get allocationContainer() { + const statusSummary = { + queuedAllocs: 0, + completeAllocs: 0, + failedAllocs: 0, + runningAllocs: 0, + startingAllocs: 0, + lostAllocs: 0, + }; + for (const allocation of this.args.node.model[this.id]) { + const { clientStatus } = allocation; + switch (clientStatus) { + // add missing statuses + case 'running': + statusSummary.runningAllocs++; + break; + case 'lost': + statusSummary.lostAllocs++; + break; + case 'failed': + statusSummary.failedAllocs++; + break; + case 'completed': + statusSummary.completeAllocs++; + break; + } + } + const Allocations = EmberObject.extend({ + ...statusSummary, + }); + return Allocations.create(); + } +} diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js new file mode 100644 index 000000000000..d25905f98f11 --- /dev/null +++ b/ui/app/controllers/jobs/job/clients.js @@ -0,0 +1,67 @@ +import Controller from '@ember/controller'; +import { action, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import Sortable from 'nomad-ui/mixins/sortable'; +import Searchable from 'nomad-ui/mixins/searchable'; +import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; + +export default class ClientsController extends Controller.extend( + Sortable, + Searchable, + WithNamespaceResetting +) { + queryParams = [ + { + currentPage: 'page', + }, + { + searchTerm: 'search', + }, + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + ]; + + currentPage = 1; + pageSize = 25; + + sortProperty = 'modifyIndex'; + sortDescending = true; + + @alias('model') job; + @jobClientStatus('nodes', 'job.status', 'job.allocations') jobClientStatus; + + @alias('uniqueNodes') listToSort; + @alias('listSorted') listToSearch; + @alias('listSearched') sortedClients; + + @computed('job') + get uniqueNodes() { + // add datacenter filter + const allocs = this.job.allocations; + const nodes = allocs.mapBy('node'); + const uniqueNodes = nodes.uniqBy('id').toArray(); + const result = uniqueNodes.map(nodeId => { + return { + [nodeId.get('id')]: allocs + .toArray() + .filter(alloc => nodeId.get('id') === alloc.get('node.id')) + .map(alloc => ({ + nodeId, + ...alloc.getProperties('clientStatus', 'name', 'createTime', 'modifyTime'), + })), + }; + }); + return result; + } + + @action + gotoClient(client) { + console.log('goToClient', client); + this.transitionToRoute('clients.client', client); + } +} diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 25b37f4aeeb5..0db12bd24c74 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -28,7 +28,6 @@ export default class IndexController extends Controller.extend(WithNamespaceRese .map(alloc => alloc.getProperties('clientStatus', 'name', 'createTime', 'modifyTime')), }; }); - console.log('result\n\n', result); return result; } diff --git a/ui/app/router.js b/ui/app/router.js index 072291fe673a..8e5c2070895b 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -23,6 +23,7 @@ Router.map(function() { this.route('dispatch'); this.route('evaluations'); this.route('allocations'); + this.route('clients'); }); }); diff --git a/ui/app/routes/jobs/job/clients.js b/ui/app/routes/jobs/job/clients.js new file mode 100644 index 000000000000..0558daf6b480 --- /dev/null +++ b/ui/app/routes/jobs/job/clients.js @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class ClientsRoute extends Route {} diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index a655756d8622..fd4fc055cfad 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -8,5 +8,6 @@ {{/if}}
    1. Allocations
    2. Evaluations
    3. +
    4. Clients
    5. diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs new file mode 100644 index 000000000000..8d91a7d930a7 --- /dev/null +++ b/ui/app/templates/jobs/job/clients.hbs @@ -0,0 +1,71 @@ +{{page-title "Job " this.job.name " clients"}} + +
      + {{#if this.uniqueNodes.length}} +
      +
      + +
      +
      + {{#if this.sortedClients}} + + + + + ID + Name + Created + Modified + Status + Allocation Summary + + + {{!-- {{log this.sortedClients.length}} --}} + + + +
      + +
      +
      + {{else}} +
      +
      +

      No Matches

      +

      No clients match the term {{this.searchTerm}}

      +
      +
      + {{/if}} + {{else}} +
      +
      +

      No Clients

      +

      No clients have been placed.

      +
      +
      + {{/if}} +
      \ No newline at end of file diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index f050335489dd..ca1ebec57fe0 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -14,6 +14,14 @@ export default function jobClientStatus(nodesKey, jobStatusKey, jobAllocsKey) { return { byNode: { '123': 'running', + '14b8ff31-8310-4877-b3e9-ba6ebcbf37a2': 'running', + '193538ac-30d7-48eb-9a31-b89a83abae1d': 'degraded', + '0cdce07c-6c50-4364-9ffd-e08c419f93aa': 'lost', + 'a07563e7-fdc6-4df4-bfd7-82b97a3605ab': 'failed', + '6c32bb4d-6fda-4c0d-a7be-008f857d9f0e': 'queued', + '75930e1c-15f2-4411-8dbd-1016e3d54c12': 'running', + '8f834f8f-75cc-4da7-96f2-3665bd9bf774': 'running', + '386a8b5f-68ab-4baf-a5ca-597d1bed8589': 'running', }, byStatus: { running: ['123'], From 3c50ac7aa8630b87f0c233cb38c5bed9d2718d0f Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 27 Aug 2021 17:29:44 -0400 Subject: [PATCH 08/58] create sysbatch pages and components --- .../job-page/parts/job-client-summary.js | 25 +++++++++ ui/app/components/job-page/parts/summary.js | 3 + ui/app/components/job-page/sysbatch.js | 15 +++++ ui/app/components/job-page/system.js | 12 +++- ui/app/controllers/jobs/job/clients.js | 6 +- ui/app/controllers/jobs/job/index.js | 9 --- ui/app/models/job.js | 2 +- ui/app/routes/jobs/job/clients.js | 4 +- .../job-page/parts/job-client-summary.hbs | 35 ++++++++++++ .../components/job-page/parts/summary.hbs | 55 ------------------- .../templates/components/job-page/service.hbs | 2 +- .../components/job-page/sysbatch.hbs | 29 ++++++++++ .../templates/components/job-page/system.hbs | 4 +- ui/app/templates/jobs/job/index.hbs | 1 - 14 files changed, 131 insertions(+), 71 deletions(-) create mode 100644 ui/app/components/job-page/parts/job-client-summary.js create mode 100644 ui/app/components/job-page/sysbatch.js create mode 100644 ui/app/templates/components/job-page/parts/job-client-summary.hbs create mode 100644 ui/app/templates/components/job-page/sysbatch.hbs diff --git a/ui/app/components/job-page/parts/job-client-summary.js b/ui/app/components/job-page/parts/job-client-summary.js new file mode 100644 index 000000000000..4e64fea40030 --- /dev/null +++ b/ui/app/components/job-page/parts/job-client-summary.js @@ -0,0 +1,25 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { classNames } from '@ember-decorators/component'; +import classic from 'ember-classic-decorator'; + +@classic +@classNames('boxed-section') +export default class JobClientSummary extends Component { + @service store; + + job = null; + jobClientStatus = null; + + @computed + get isExpanded() { + const storageValue = window.localStorage.nomadExpandJobClientSummary; + return storageValue != null ? JSON.parse(storageValue) : true; + } + + persist(item, isOpen) { + window.localStorage.nomadExpandJobClientSummary = isOpen; + this.notifyPropertyChange('isExpanded'); + } +} diff --git a/ui/app/components/job-page/parts/summary.js b/ui/app/components/job-page/parts/summary.js index 87ca0a4c38ab..f42b909e2461 100644 --- a/ui/app/components/job-page/parts/summary.js +++ b/ui/app/components/job-page/parts/summary.js @@ -7,9 +7,12 @@ import classic from 'ember-classic-decorator'; @classNames('boxed-section') export default class Summary extends Component { job = null; + forceCollapsed = false; @computed get isExpanded() { + if (this.forceCollapsed) return false; + const storageValue = window.localStorage.nomadExpandJobSummary; return storageValue != null ? JSON.parse(storageValue) : true; } diff --git a/ui/app/components/job-page/sysbatch.js b/ui/app/components/job-page/sysbatch.js new file mode 100644 index 000000000000..0819ed494266 --- /dev/null +++ b/ui/app/components/job-page/sysbatch.js @@ -0,0 +1,15 @@ +import AbstractJobPage from './abstract'; +import classic from 'ember-classic-decorator'; +import { inject as service } from '@ember/service'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; + +@classic +export default class Sysbatch extends AbstractJobPage { + @service store; + + @jobClientStatus('nodes', 'job') jobClientStatus; + + get nodes() { + return this.store.peekAll('node'); + } +} diff --git a/ui/app/components/job-page/system.js b/ui/app/components/job-page/system.js index bf2c04442466..5909c8f163d1 100644 --- a/ui/app/components/job-page/system.js +++ b/ui/app/components/job-page/system.js @@ -1,5 +1,15 @@ import AbstractJobPage from './abstract'; import classic from 'ember-classic-decorator'; +import { inject as service } from '@ember/service'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic -export default class System extends AbstractJobPage {} +export default class System extends AbstractJobPage { + @service store; + + @jobClientStatus('nodes', 'job') jobClientStatus; + + get nodes() { + return this.store.peekAll('node'); + } +} diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index d25905f98f11..753270f9354c 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -33,7 +33,7 @@ export default class ClientsController extends Controller.extend( sortDescending = true; @alias('model') job; - @jobClientStatus('nodes', 'job.status', 'job.allocations') jobClientStatus; + @jobClientStatus('nodes', 'job') jobClientStatus; @alias('uniqueNodes') listToSort; @alias('listSorted') listToSearch; @@ -59,6 +59,10 @@ export default class ClientsController extends Controller.extend( return result; } + get nodes() { + return this.store.peekAll('node'); + } + @action gotoClient(client) { console.log('goToClient', client); diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index d59a8bf0ae31..eae91858f7a6 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -4,19 +4,10 @@ import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { action } from '@ember/object'; import classic from 'ember-classic-decorator'; -import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic export default class IndexController extends Controller.extend(WithNamespaceResetting) { @service system; - @service store; - - @jobClientStatus('nodes', 'job') jobClientStatus; - - // TODO: use watch - get nodes() { - return this.store.peekAll('node'); - } queryParams = [ { diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 79323d280aed..922b8a86ed8e 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -7,7 +7,7 @@ import RSVP from 'rsvp'; import { assert } from '@ember/debug'; import classic from 'ember-classic-decorator'; -const JOB_TYPES = ['service', 'batch', 'system']; +const JOB_TYPES = ['service', 'batch', 'system', 'sysbatch']; @classic export default class Job extends Model { diff --git a/ui/app/routes/jobs/job/clients.js b/ui/app/routes/jobs/job/clients.js index 0558daf6b480..0be962399834 100644 --- a/ui/app/routes/jobs/job/clients.js +++ b/ui/app/routes/jobs/job/clients.js @@ -1,3 +1,5 @@ import Route from '@ember/routing/route'; -export default class ClientsRoute extends Route {} +export default class ClientsRoute extends Route { + // TODO: add watcher for nodes. +} diff --git a/ui/app/templates/components/job-page/parts/job-client-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-summary.hbs new file mode 100644 index 000000000000..709a68e968e5 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/job-client-summary.hbs @@ -0,0 +1,35 @@ + + +
      +
      + Job Status in Client + + {{this.jobClientStatus.totalNodes}} + +
      + + {{#unless a.isOpen}} +
      +
      + +
      +
      + {{/unless}} +
      +
      + + +
        + {{#each chart.data as |datum index|}} +
      1. + + {{datum.value}} + + {{datum.label}} + +
      2. + {{/each}} +
      +
      +
      +
      \ No newline at end of file diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index 79324a2a238f..0fe77d45b94d 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -1,59 +1,4 @@ - -
      -
      - {{#if a.item.hasChildren}} - Children Status - - {{a.item.summary.totalChildren}} - - {{else}} - Client Status - - {{this.jobClientStatus.totalNodes}} - - {{/if}} -
      - - {{#unless a.isOpen}} -
      -
      - {{#if a.item.hasChildren}} - {{#if (gt a.item.totalChildren 0)}} - - {{else}} - No Children - {{/if}} - {{else}} - - {{/if}} -
      -
      - {{/unless}} -
      -
      - - {{#component (if a.item.hasChildren "children-status-bar" "client-status-bar") - jobClientStatus=this.jobClientStatus - 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/service.hbs b/ui/app/templates/components/job-page/service.hbs index c80e664d3450..d8180f244a8c 100644 --- a/ui/app/templates/components/job-page/service.hbs +++ b/ui/app/templates/components/job-page/service.hbs @@ -19,7 +19,7 @@ {{/each}} {{/if}} - + diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs new file mode 100644 index 000000000000..de3602b486b4 --- /dev/null +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -0,0 +1,29 @@ + + + + + +
      +
      + Type: {{this.job.type}} | + Priority: {{this.job.priority}} + {{#if (and this.job.namespace this.system.shouldShowNamespaces)}} + | Namespace: {{this.job.namespace.name}} + {{/if}} +
      +
      + + + + + + + + + + +
      diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 4b97c0067c87..66d5db0e43eb 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -19,7 +19,9 @@ {{/each}} {{/if}} - + + + diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index ea009c855b5b..56d18ec2e57c 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -1,7 +1,6 @@ {{page-title "Job " this.model.name}} {{component (concat "job-page/" this.model.templateType) job=this.model - jobClientStatus=this.jobClientStatus sortProperty=this.sortProperty sortDescending=this.sortDescending currentPage=this.currentPage From 91a17cb1b47ac08e37536de91e3aa0c00b140ce7 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 27 Aug 2021 17:34:17 -0400 Subject: [PATCH 09/58] add line break --- .../templates/components/job-page/parts/job-client-summary.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/templates/components/job-page/parts/job-client-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-summary.hbs index 709a68e968e5..fbbe00d043af 100644 --- a/ui/app/templates/components/job-page/parts/job-client-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-summary.hbs @@ -32,4 +32,4 @@
    - \ No newline at end of file + From 6444f6ac101160e8223f6a89c110efe2ef421e1e Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 1 Sep 2021 19:06:39 -0400 Subject: [PATCH 10/58] filtering in client tab --- ui/.prettierrc | 16 +++- ui/app/components/client-row.hbs | 59 +++++++------- ui/app/components/client-row.js | 44 ++++++++--- ui/app/controllers/jobs/job/clients.js | 85 +++++++++++++++----- ui/app/templates/jobs/job/clients.hbs | 103 ++++++++++++++++++------- 5 files changed, 219 insertions(+), 88 deletions(-) diff --git a/ui/.prettierrc b/ui/.prettierrc index 7f9eaa64f7f7..546369158ebd 100644 --- a/ui/.prettierrc +++ b/ui/.prettierrc @@ -1,3 +1,13 @@ -printWidth: 100 -singleQuote: true -trailingComma: es5 +{ + "printWidth": 100, + "singleQuote": true, + "trailingComma": "es5", + "overrides": [ + { + "files": "*.hbs", + "options": { + "singleQuote": false + } + } + ] +} diff --git a/ui/app/components/client-row.hbs b/ui/app/components/client-row.hbs index 0b2c2defe079..6b331090272f 100644 --- a/ui/app/components/client-row.hbs +++ b/ui/app/components/client-row.hbs @@ -1,27 +1,34 @@ - - - - - {{this.id}} - - - - {{this.name}} - -{{moment-from-now this.eldestCreateTime}} - - - {{moment-from-now this.allocation.modifyTime}} - - - - {{this.status}} - - -
    - -
    - - - + + + + {{this.node.shortId}} + + + + {{this.node.name}} + + + + {{moment-from-now this.eldestCreateTime}} + + + + + {{moment-from-now this.allocation.modifyTime}} + + + + + {{this.status}} + + + {{#if this.shouldDisplayAllocationSummary}} +
    + +
    + {{else}} + Not Scheduled + {{/if}} + + \ No newline at end of file diff --git a/ui/app/components/client-row.js b/ui/app/components/client-row.js index 5c84a42fb5c5..96ee493cda34 100644 --- a/ui/app/components/client-row.js +++ b/ui/app/components/client-row.js @@ -5,18 +5,16 @@ import Component from '@glimmer/component'; @tagName('tr') @classNames('client-row', 'is-interactive') export default class ClientRowComponent extends Component { - get id() { - console.log('node arg', this.args.node); - return Object.keys(this.args.node.model)[0]; + get shouldDisplayAllocationSummary() { + return this.status !== 'notScheduled'; } - - get name() { - return this.args.node.model[this.id][0].name; + get node() { + return this.args.node.model; } get eldestCreateTime() { let eldest = null; - for (const allocation of this.args.node.model[this.id]) { + for (const allocation of this.node.id) { if (!eldest || allocation.createTime < eldest) { eldest = allocation.createTime; } @@ -26,7 +24,7 @@ export default class ClientRowComponent extends Component { get mostRecentModifyTime() { let mostRecent = null; - for (const allocation of this.args.node.model[this.id]) { + for (const allocation of this.node.id) { if (!mostRecent || allocation.modifyTime > mostRecent) { mostRecent = allocation.createTime; } @@ -35,7 +33,7 @@ export default class ClientRowComponent extends Component { } get status() { - return this.args.jobClientStatus.byNode[this.id]; + return this.args.jobClientStatus.byNode[this.node.id]; } get allocationContainer() { @@ -47,10 +45,29 @@ export default class ClientRowComponent extends Component { startingAllocs: 0, lostAllocs: 0, }; - for (const allocation of this.args.node.model[this.id]) { + // query by allocations for job then group by node use the mapBy method + if (this.status === 'notScheduled') return EmberObject.create(...statusSummary); + + const allocsByNodeID = {}; + this.args.allocations.forEach(a => { + const nodeId = a.node.get('id'); + if (!allocsByNodeID[nodeId]) { + allocsByNodeID[nodeId] = []; + } + allocsByNodeID[nodeId].push(a); + }); + for (const allocation of allocsByNodeID[this.node.id]) { + if (this.status === 'queued') { + statusSummary.queuedAllocs = allocsByNodeID[this.node.id].length; + break; + } else if (this.status === 'starting') { + statusSummary.startingAllocs = allocsByNodeID[this.node.id].length; + break; + } else if (this.status === 'notScheduled') { + break; + } const { clientStatus } = allocation; switch (clientStatus) { - // add missing statuses case 'running': statusSummary.runningAllocs++; break; @@ -60,9 +77,12 @@ export default class ClientRowComponent extends Component { case 'failed': statusSummary.failedAllocs++; break; - case 'completed': + case 'complete': statusSummary.completeAllocs++; break; + case 'starting': + statusSummary.startingAllocs++; + break; } } const Allocations = EmberObject.extend({ diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index 753270f9354c..f188395045a5 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -1,10 +1,13 @@ import Controller from '@ember/controller'; import { action, computed } from '@ember/object'; +import { scheduleOnce } from '@ember/runloop'; +import intersection from 'lodash.intersection'; import { alias } from '@ember/object/computed'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; +import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize'; export default class ClientsController extends Controller.extend( Sortable, @@ -18,6 +21,12 @@ export default class ClientsController extends Controller.extend( { searchTerm: 'search', }, + { + qpStatus: 'status', + }, + { + qpDatacenter: 'dc', + }, { sortProperty: 'sort', }, @@ -32,40 +41,74 @@ export default class ClientsController extends Controller.extend( sortProperty = 'modifyIndex'; sortDescending = true; + @computed + get searchProps() { + return ['id', 'name', 'taskGroupName']; + } + + qpStatus = ''; + qpDatacenter = ''; + + @selection('qpStatus') selectionStatus; + @selection('qpDatacenter') selectionDatacenter; + + @computed + get optionsStatus() { + return [ + { key: 'queued', label: 'Queued' }, + { key: 'notScheduled', label: 'Not Scheduled' }, + { key: 'starting', label: 'Starting' }, + { key: 'running', label: 'Running' }, + { key: 'complete', label: 'Complete' }, + { key: 'degraded', label: 'Degraded' }, + { key: 'failed', label: 'Failed' }, + { key: 'lost', label: 'Lost' }, + ]; + } + @alias('model') job; @jobClientStatus('nodes', 'job') jobClientStatus; - @alias('uniqueNodes') listToSort; + @alias('filteredNodes') listToSort; @alias('listSorted') listToSearch; @alias('listSearched') sortedClients; - @computed('job') - get uniqueNodes() { - // add datacenter filter - const allocs = this.job.allocations; - const nodes = allocs.mapBy('node'); - const uniqueNodes = nodes.uniqBy('id').toArray(); - const result = uniqueNodes.map(nodeId => { - return { - [nodeId.get('id')]: allocs - .toArray() - .filter(alloc => nodeId.get('id') === alloc.get('node.id')) - .map(alloc => ({ - nodeId, - ...alloc.getProperties('clientStatus', 'name', 'createTime', 'modifyTime'), - })), - }; + get nodes() { + return this.store.peekAll('node'); + } + + @computed('nodes', 'selectionStatus') + get filteredNodes() { + const { selectionStatus: statuses } = this; + + return this.nodes.filter(node => { + if (statuses.length && !statuses.includes(this.jobClientStatus.byNode[node.id])) { + return false; + } + return true; }); - return result; } - get nodes() { - return this.store.peekAll('node'); + @computed('selectionDatacenter', 'job.datacenters') + get optionsDatacenter() { + // eslint-disable-next-line ember/no-incorrect-calls-with-inline-anonymous-functions + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set( + this.qpDatacenter, + serialize(intersection(this.job.datacenters, this.selectionDatacenter)) + ); + }); + + return this.job.datacenters.sort().map(dc => ({ key: dc, label: dc })); } @action gotoClient(client) { - console.log('goToClient', client); this.transitionToRoute('clients.client', client); } + + setFacetQueryParam(queryParam, selection) { + this.set(queryParam, serialize(selection)); + } } diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index 8d91a7d930a7..708eed4ddb13 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -1,14 +1,33 @@ {{page-title "Job " this.job.name " clients"}}
    - {{#if this.uniqueNodes.length}} -
    -
    + {{#if this.nodes.length}} +
    +
    + @placeholder="Search clients..." + /> +
    +
    +
    + + +
    {{#if this.sortedClients}} @@ -16,38 +35,59 @@ @source={{this.sortedClients}} @size={{this.pageSize}} @page={{this.currentPage}} - @class="clients" as |p|> + @class="clients" as |p| + > + @class="with-foot" as |t| + > - ID - Name - Created - Modified - Status - Allocation Summary + + Client ID + + + Client Name + + + Created + + + Modified + + + Job Status + + + Allocation Summary + - {{!-- {{log this.sortedClients.length}} --}} - +
    @@ -55,16 +95,27 @@ {{else}}
    -

    No Matches

    -

    No clients match the term {{this.searchTerm}}

    +

    + No Matches +

    +

    + No clients match the term + + {{this.searchTerm}} + +

    {{/if}} {{else}}
    -

    No Clients

    -

    No clients have been placed.

    +

    + No Clients +

    +

    + No clients have been placed. +

    {{/if}} From c0171a405a4aaf8c0304e00cc433739dfac16a10 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Thu, 2 Sep 2021 11:09:08 -0400 Subject: [PATCH 11/58] svg link --- ui/app/controllers/jobs/job/clients.js | 17 +++++++++ .../templates/components/distribution-bar.hbs | 38 +++++++++++-------- ui/app/templates/jobs/job/clients.hbs | 7 ++++ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index f188395045a5..2b802068bf2f 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -27,6 +27,9 @@ export default class ClientsController extends Controller.extend( { qpDatacenter: 'dc', }, + { + qpNodeClass: 'nodeClass', + }, { sortProperty: 'sort', }, @@ -48,9 +51,11 @@ export default class ClientsController extends Controller.extend( qpStatus = ''; qpDatacenter = ''; + qpNodeClass = ''; @selection('qpStatus') selectionStatus; @selection('qpDatacenter') selectionDatacenter; + @selection('qpNodeClass') selectionNodeClass; @computed get optionsStatus() { @@ -103,6 +108,18 @@ export default class ClientsController extends Controller.extend( return this.job.datacenters.sort().map(dc => ({ key: dc, label: dc })); } + @computed('selectionNodeClass', 'nodes') + get optionsNodeClass() { + const nodeClasses = Array.from(new Set(this.nodes.mapBy('nodeClass'))); + // eslint-disable-next-line ember/no-incorrect-calls-with-inline-anonymous-functions + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set(this.qpNodeClass, serialize(intersection(nodeClasses, this.selectionNodeClassß))); + }); + + return nodeClasses.sort().map(nodeClass => ({ key: nodeClass, label: nodeClass })); + } + @action gotoClient(client) { this.transitionToRoute('clients.client', client); diff --git a/ui/app/templates/components/distribution-bar.hbs b/ui/app/templates/components/distribution-bar.hbs index 9f42b9c7c70e..611f79e8662b 100644 --- a/ui/app/templates/components/distribution-bar.hbs +++ b/ui/app/templates/components/distribution-bar.hbs @@ -1,28 +1,34 @@ - - - - - - - - + + + + + + + + + + {{#if hasBlock}} - {{yield (hash - data=this._data - activeDatum=this.activeDatum - )}} + {{yield (hash data=this._data activeDatum=this.activeDatum)}} {{else}} -
    +
      {{#each this._data as |datum index|}}
    1. - + {{datum.label}} - {{datum.value}} + + {{datum.value}} +
    2. {{/each}}
    -{{/if}} +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index 708eed4ddb13..e94d8863673b 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -27,6 +27,13 @@ @selection={{this.selectionDatacenter}} @onSelect={{action this.setFacetQueryParam "qpDatacenter"}} /> +
    From d6dd911986aaadb7f19c886a0cec01fcbc1d4c64 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Tue, 7 Sep 2021 15:38:42 -0400 Subject: [PATCH 12/58] refactor client-status-bar to append chart with links --- ui/app/components/client-status-bar.js | 21 +++++++- ui/app/controllers/jobs/job/clients.js | 29 +++++++---- ui/app/controllers/jobs/job/index.js | 6 +++ ui/app/styles/charts/distribution-bar.scss | 1 + .../templates/components/distribution-bar.hbs | 18 ++++--- .../job-page/parts/job-client-summary.hbs | 33 ++++++++++--- .../components/job-page/sysbatch.hbs | 48 +++++++++++++------ ui/app/templates/jobs/job/index.hbs | 7 ++- ui/config/environment.js | 2 +- ui/mirage/scenarios/sysbatch.js | 2 +- 10 files changed, 122 insertions(+), 45 deletions(-) diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js index 65d4a2335e6a..937da6c78c08 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/client-status-bar.js @@ -1,4 +1,4 @@ -import { computed } from '@ember/object'; +import { computed, set } from '@ember/object'; import DistributionBar from './distribution-bar'; import classic from 'ember-classic-decorator'; @@ -9,6 +9,25 @@ export default class ClientStatusBar extends DistributionBar { 'data-test-client-status-bar' = true; jobClientStatus = null; + // Provide an action with access to the router + gotoClient() {} + + didUpdateAttrs() { + const { _data, chart } = this; + const filteredData = _data.filter(d => d.value > 0); + filteredData.forEach((d, index) => { + set(d, 'index', index); + }); + chart + .select('.bars') + .selectAll('g') + .data(filteredData, d => d.label) + .on('click', d => { + let label = d.label === 'Not Scheduled' ? 'notScheduled' : d.label; + this.gotoClient(label); + }); + } + @computed('jobClientStatus') get data() { return [ diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index 2b802068bf2f..552813c0a28f 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -8,7 +8,9 @@ import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize'; +import classic from 'ember-classic-decorator'; +@classic export default class ClientsController extends Controller.extend( Sortable, Searchable, @@ -28,7 +30,7 @@ export default class ClientsController extends Controller.extend( qpDatacenter: 'dc', }, { - qpNodeClass: 'nodeClass', + qpNodeClass: 'nodeclass', }, { sortProperty: 'sort', @@ -82,14 +84,25 @@ export default class ClientsController extends Controller.extend( return this.store.peekAll('node'); } - @computed('nodes', 'selectionStatus') + @computed('nodes', 'selectionStatus', 'selectionDatacenter', 'selectionNodeClass') get filteredNodes() { - const { selectionStatus: statuses } = this; + const { + selectionStatus: statuses, + selectionDatacenter: datacenters, + selectionNodeClass: nodeclasses, + } = this; return this.nodes.filter(node => { if (statuses.length && !statuses.includes(this.jobClientStatus.byNode[node.id])) { return false; } + if (datacenters.length && !datacenters.includes(node.datacenter)) { + return false; + } + if (nodeclasses.length && !nodeclasses.includes(node.nodeClass)) { + return false; + } + return true; }); } @@ -111,11 +124,11 @@ export default class ClientsController extends Controller.extend( @computed('selectionNodeClass', 'nodes') get optionsNodeClass() { const nodeClasses = Array.from(new Set(this.nodes.mapBy('nodeClass'))); - // eslint-disable-next-line ember/no-incorrect-calls-with-inline-anonymous-functions - scheduleOnce('actions', () => { - // eslint-disable-next-line ember/no-side-effects - this.set(this.qpNodeClass, serialize(intersection(nodeClasses, this.selectionNodeClassß))); - }); + // // eslint-disable-next-line ember/no-incorrect-calls-with-inline-anonymous-functions + // scheduleOnce('actions', () => { + // // eslint-disable-next-line ember/no-side-effects + // this.set(this.qpNodeClass, serialize(intersection(nodeClasses, this.selectionNodeClass))); + // }); return nodeClasses.sort().map(nodeClass => ({ key: nodeClass, label: nodeClass })); } diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index eae91858f7a6..6c580336a5e2 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -39,4 +39,10 @@ export default class IndexController extends Controller.extend(WithNamespaceRese queryParams: { jobNamespace: job.get('namespace.name') }, }); } + + @action + gotoClient(d) { + const encodeStatus = JSON.stringify([`${d.charAt(0).toLowerCase()}${d.slice(1)}`]); + this.transitionToRoute('jobs.job.clients', this.job, { queryParams: { status: encodeStatus } }); + } } diff --git a/ui/app/styles/charts/distribution-bar.scss b/ui/app/styles/charts/distribution-bar.scss index fcd4a782425f..7620615f43c5 100644 --- a/ui/app/styles/charts/distribution-bar.scss +++ b/ui/app/styles/charts/distribution-bar.scss @@ -8,6 +8,7 @@ width: 100%; .bars { + cursor: pointer; rect { transition: opacity 0.3s ease-in-out; opacity: 1; diff --git a/ui/app/templates/components/distribution-bar.hbs b/ui/app/templates/components/distribution-bar.hbs index 611f79e8662b..08c3d5f0fc70 100644 --- a/ui/app/templates/components/distribution-bar.hbs +++ b/ui/app/templates/components/distribution-bar.hbs @@ -1,13 +1,11 @@ - - - - - - - - - - + + + + + + + + {{#if hasBlock}} {{yield (hash data=this._data activeDatum=this.activeDatum)}} {{else}} diff --git a/ui/app/templates/components/job-page/parts/job-client-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-summary.hbs index fbbe00d043af..c68e485cf74c 100644 --- a/ui/app/templates/components/job-page/parts/job-client-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-summary.hbs @@ -1,4 +1,10 @@ - +
    @@ -7,7 +13,6 @@ {{this.jobClientStatus.totalNodes}}
    - {{#unless a.isOpen}}
    @@ -18,12 +23,26 @@
    - +
      {{#each chart.data as |datum index|}} -
    1. - - {{datum.value}} +
    2. + + + {{datum.value}} + {{datum.label}} @@ -32,4 +51,4 @@
    - + \ No newline at end of file diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs index de3602b486b4..d4a7895fd5da 100644 --- a/ui/app/templates/components/job-page/sysbatch.hbs +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -1,29 +1,47 @@ - - + -
    - Type: {{this.job.type}} | - Priority: {{this.job.priority}} + + + Type: + + {{this.job.type}} + | + + + + Priority: + + {{this.job.priority}} + {{#if (and this.job.namespace this.system.shouldShowNamespaces)}} - | Namespace: {{this.job.namespace.name}} + + | + + Namespace: + + {{this.job.namespace.name}} + {{/if}}
    - - - - - + + - - + @gotoTaskGroup={{this.gotoTaskGroup}} + /> -
    + \ No newline at end of file diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index 56d18ec2e57c..53d3e6ecbf26 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -1,8 +1,11 @@ {{page-title "Job " this.model.name}} -{{component (concat "job-page/" this.model.templateType) +{{component + (concat "job-page/" this.model.templateType) job=this.model sortProperty=this.sortProperty sortDescending=this.sortDescending currentPage=this.currentPage gotoJob=(action "gotoJob") - gotoTaskGroup=(action "gotoTaskGroup")}} + gotoClient=(action "gotoClient") + gotoTaskGroup=(action "gotoTaskGroup") +}} \ No newline at end of file diff --git a/ui/config/environment.js b/ui/config/environment.js index 331bfdfb8a1d..d5ea567f5a93 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -26,7 +26,7 @@ module.exports = function(environment) { APP: { blockingQueries: true, // TODO: revert before merging to main. - mirageScenario: 'sysbatchSmall', + mirageScenario: 'topoMedium', // convert to 'sysbatchSmall' when working on feature mirageWithNamespaces: false, mirageWithTokens: true, mirageWithRegions: true, diff --git a/ui/mirage/scenarios/sysbatch.js b/ui/mirage/scenarios/sysbatch.js index 1f4c071f52aa..7905585e21fc 100644 --- a/ui/mirage/scenarios/sysbatch.js +++ b/ui/mirage/scenarios/sysbatch.js @@ -8,7 +8,7 @@ export function sysbatchSmall(server) { // Job with 1 task group. const job1 = server.create('job', { status: 'running', - datacenters: ['dc1'], + datacenters: ['dc1', 'dc2'], type: 'sysbatch', resourceSpec: ['M: 256, C: 500'], createAllocations: false, From 2a2902d30073cf7dd386a5dff7b493ebbc34cbf8 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 8 Sep 2021 09:39:45 -0400 Subject: [PATCH 13/58] unit test --- ui/tests/unit/utils/job-client-status-test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 ui/tests/unit/utils/job-client-status-test.js diff --git a/ui/tests/unit/utils/job-client-status-test.js b/ui/tests/unit/utils/job-client-status-test.js new file mode 100644 index 000000000000..c2fa20e530b2 --- /dev/null +++ b/ui/tests/unit/utils/job-client-status-test.js @@ -0,0 +1,18 @@ +import { module, test } from 'qunit'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { jobClientStatus } from 'nomad-ui/utils/job-client-status'; + +module('Unit | Util | jobclientstatus', function(hooks) { + hooks.beforeEach(async function() { + console.log('hi'); + this.server = startMirage(); + }); + hooks.afterEach(async function() { + this.server.shutdown(); + }); + test('some test', async function(assert) { + // this.pauseTest(); + // jobClientStatus('node', 'job'); + assert.equal(0, 0); + }); +}); From d89d071b2c4aa89288f7791c9d0aaf8112bc6c7f Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 10 Sep 2021 19:02:38 -0400 Subject: [PATCH 14/58] ui: policy system and sysbatch job details clients tab --- ui/app/components/allocation-row.js | 4 +- ui/app/components/client-row.hbs | 35 ++-- ui/app/components/client-row.js | 114 ++++++------- ui/app/components/task-row.js | 4 +- ui/app/controllers/jobs/job/clients.js | 158 +++++++++++------- ui/app/routes/jobs/job/clients.js | 29 +++- ui/app/styles/components.scss | 1 + .../components/job-client-status-row.scss | 9 + ui/app/templates/jobs/job/clients.hbs | 20 +-- ui/mirage/scenarios/sysbatch.js | 6 + 10 files changed, 234 insertions(+), 146 deletions(-) create mode 100644 ui/app/styles/components/job-client-status-row.scss diff --git a/ui/app/components/allocation-row.js b/ui/app/components/allocation-row.js index 0a9fb1c0a0a9..a92c618adaec 100644 --- a/ui/app/components/allocation-row.js +++ b/ui/app/components/allocation-row.js @@ -65,7 +65,9 @@ export default class AllocationRow extends Component { do { if (this.stats) { try { - yield this.get('stats.poll').perform(); + yield this.get('stats.poll') + .linked() + .perform(); this.set('statsError', false); } catch (error) { this.set('statsError', true); diff --git a/ui/app/components/client-row.hbs b/ui/app/components/client-row.hbs index 6b331090272f..d0094ab1872e 100644 --- a/ui/app/components/client-row.hbs +++ b/ui/app/components/client-row.hbs @@ -1,34 +1,39 @@ - - + - - {{this.node.shortId}} - + {{this.row.node.shortId}} - - {{this.node.name}} + + {{this.row.node.name}} - - {{moment-from-now this.eldestCreateTime}} + {{#if this.row.createTime}} + + {{moment-from-now this.row.createTime}} + {{else}} + - + {{/if}} - - {{moment-from-now this.allocation.modifyTime}} + {{#if this.row.modifyTime}} + + {{moment-from-now this.row.modifyTime}} + {{else}} + - + {{/if}} - - {{this.status}} + + {{this.humanizedJobStatus}} - + {{#if this.shouldDisplayAllocationSummary}}
    {{else}} - Not Scheduled +
    {{this.allocationSummaryPlaceholder}}
    {{/if}} \ No newline at end of file diff --git a/ui/app/components/client-row.js b/ui/app/components/client-row.js index 96ee493cda34..dc467fbf5d87 100644 --- a/ui/app/components/client-row.js +++ b/ui/app/components/client-row.js @@ -1,39 +1,43 @@ -import { classNames, tagName } from '@ember-decorators/component'; import EmberObject from '@ember/object'; import Component from '@glimmer/component'; -@tagName('tr') -@classNames('client-row', 'is-interactive') -export default class ClientRowComponent extends Component { - get shouldDisplayAllocationSummary() { - return this.status !== 'notScheduled'; +export default class ClientRow extends Component { + // Attribute set in the template as @onClick. + onClick() {} + + get row() { + return this.args.row.model; } - get node() { - return this.args.node.model; + + get shouldDisplayAllocationSummary() { + return this.args.row.model.jobStatus !== 'notScheduled'; } - get eldestCreateTime() { - let eldest = null; - for (const allocation of this.node.id) { - if (!eldest || allocation.createTime < eldest) { - eldest = allocation.createTime; - } + get allocationSummaryPlaceholder() { + switch (this.args.row.model.jobStatus) { + case 'notScheduled': + return 'Not Scheduled'; + default: + return ''; } - return eldest; } - get mostRecentModifyTime() { - let mostRecent = null; - for (const allocation of this.node.id) { - if (!mostRecent || allocation.modifyTime > mostRecent) { - mostRecent = allocation.createTime; - } + get humanizedJobStatus() { + switch (this.args.row.model.jobStatus) { + case 'notScheduled': + return 'not scheduled'; + default: + return this.args.row.model.jobStatus; } - return mostRecent; } - get status() { - return this.args.jobClientStatus.byNode[this.node.id]; + get jobStatusClass() { + switch (this.args.row.model.jobStatus) { + case 'notScheduled': + return 'not-scheduled'; + default: + return this.args.row.model.jobStatus; + } } get allocationContainer() { @@ -45,46 +49,38 @@ export default class ClientRowComponent extends Component { startingAllocs: 0, lostAllocs: 0, }; - // query by allocations for job then group by node use the mapBy method - if (this.status === 'notScheduled') return EmberObject.create(...statusSummary); - const allocsByNodeID = {}; - this.args.allocations.forEach(a => { - const nodeId = a.node.get('id'); - if (!allocsByNodeID[nodeId]) { - allocsByNodeID[nodeId] = []; - } - allocsByNodeID[nodeId].push(a); - }); - for (const allocation of allocsByNodeID[this.node.id]) { - if (this.status === 'queued') { - statusSummary.queuedAllocs = allocsByNodeID[this.node.id].length; + switch (this.args.row.model.jobStatus) { + case 'notSchedule': break; - } else if (this.status === 'starting') { - statusSummary.startingAllocs = allocsByNodeID[this.node.id].length; + case 'queued': + statusSummary.queuedAllocs = this.args.row.model.allocations.length; break; - } else if (this.status === 'notScheduled') { + case 'starting': + statusSummary.startingAllocs = this.args.row.model.allocations.length; break; - } - const { clientStatus } = allocation; - switch (clientStatus) { - case 'running': - statusSummary.runningAllocs++; - break; - case 'lost': - statusSummary.lostAllocs++; - break; - case 'failed': - statusSummary.failedAllocs++; - break; - case 'complete': - statusSummary.completeAllocs++; - break; - case 'starting': - statusSummary.startingAllocs++; - break; - } + default: + for (const alloc of this.args.row.model.allocations) { + switch (alloc.clientStatus) { + case 'running': + statusSummary.runningAllocs++; + break; + case 'lost': + statusSummary.lostAllocs++; + break; + case 'failed': + statusSummary.failedAllocs++; + break; + case 'complete': + statusSummary.completeAllocs++; + break; + case 'starting': + statusSummary.startingAllocs++; + break; + } + } } + const Allocations = EmberObject.extend({ ...statusSummary, }); diff --git a/ui/app/components/task-row.js b/ui/app/components/task-row.js index cde652fa7ab4..e47ff85883db 100644 --- a/ui/app/components/task-row.js +++ b/ui/app/components/task-row.js @@ -54,7 +54,9 @@ export default class TaskRow extends Component { do { if (this.stats) { try { - yield this.get('stats.poll').perform(); + yield this.get('stats.poll') + .linked() + .perform(); this.set('statsError', false); } catch (error) { this.set('statsError', true); diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index 552813c0a28f..ad46d9d6e5d5 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -1,9 +1,10 @@ +/* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ import Controller from '@ember/controller'; import { action, computed } from '@ember/object'; import { scheduleOnce } from '@ember/runloop'; import intersection from 'lodash.intersection'; import { alias } from '@ember/object/computed'; -import Sortable from 'nomad-ui/mixins/sortable'; +import SortableFactory from 'nomad-ui/mixins/sortable-factory'; import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @@ -12,7 +13,7 @@ import classic from 'ember-classic-decorator'; @classic export default class ClientsController extends Controller.extend( - Sortable, + SortableFactory(['id', 'name', 'jobStatus']), Searchable, WithNamespaceResetting ) { @@ -40,51 +41,50 @@ export default class ClientsController extends Controller.extend( }, ]; - currentPage = 1; - pageSize = 25; - - sortProperty = 'modifyIndex'; - sortDescending = true; - - @computed - get searchProps() { - return ['id', 'name', 'taskGroupName']; - } - qpStatus = ''; qpDatacenter = ''; qpNodeClass = ''; + currentPage = 1; + pageSize = 25; + + sortProperty = 'jobStatus'; + sortDescending = false; + @selection('qpStatus') selectionStatus; @selection('qpDatacenter') selectionDatacenter; @selection('qpNodeClass') selectionNodeClass; - @computed - get optionsStatus() { - return [ - { key: 'queued', label: 'Queued' }, - { key: 'notScheduled', label: 'Not Scheduled' }, - { key: 'starting', label: 'Starting' }, - { key: 'running', label: 'Running' }, - { key: 'complete', label: 'Complete' }, - { key: 'degraded', label: 'Degraded' }, - { key: 'failed', label: 'Failed' }, - { key: 'lost', label: 'Lost' }, - ]; - } - @alias('model') job; - @jobClientStatus('nodes', 'job') jobClientStatus; + @jobClientStatus('allNodes', 'job') jobClientStatus; @alias('filteredNodes') listToSort; @alias('listSorted') listToSearch; @alias('listSearched') sortedClients; - get nodes() { + @computed + get allNodes() { return this.store.peekAll('node'); } - @computed('nodes', 'selectionStatus', 'selectionDatacenter', 'selectionNodeClass') + @computed('allNodes') + get nodes() { + return this.allNodes.filter(node => this.jobClientStatus.byNode[node.id]); + } + + @computed + get searchProps() { + return ['node.id', 'node.name']; + } + + @computed( + 'nodes', + 'job.allocations', + 'jobClientStatus.byNode', + 'selectionStatus', + 'selectionDatacenter', + 'selectionNodeClass' + ) get filteredNodes() { const { selectionStatus: statuses, @@ -92,43 +92,69 @@ export default class ClientsController extends Controller.extend( selectionNodeClass: nodeclasses, } = this; - return this.nodes.filter(node => { - if (statuses.length && !statuses.includes(this.jobClientStatus.byNode[node.id])) { - return false; - } - if (datacenters.length && !datacenters.includes(node.datacenter)) { - return false; - } - if (nodeclasses.length && !nodeclasses.includes(node.nodeClass)) { - return false; - } - - return true; - }); + return this.nodes + .filter(node => { + if (statuses.length && !statuses.includes(this.jobClientStatus.byNode[node.id])) { + return false; + } + if (datacenters.length && !datacenters.includes(node.datacenter)) { + return false; + } + if (nodeclasses.length && !nodeclasses.includes(node.nodeClass)) { + return false; + } + + return true; + }) + .map(node => { + const allocations = this.job.allocations.filter(alloc => alloc.get('node.id') == node.id); + + return { + node, + jobStatus: this.jobClientStatus.byNode[node.id], + allocations, + createTime: eldestCreateTime(allocations), + modifyTime: mostRecentModifyTime(allocations), + }; + }); } - @computed('selectionDatacenter', 'job.datacenters') + @computed + get optionsJobStatus() { + return [ + { key: 'queued', label: 'Queued' }, + { key: 'notScheduled', label: 'Not Scheduled' }, + { key: 'starting', label: 'Starting' }, + { key: 'running', label: 'Running' }, + { key: 'complete', label: 'Complete' }, + { key: 'degraded', label: 'Degraded' }, + { key: 'failed', label: 'Failed' }, + { key: 'lost', label: 'Lost' }, + ]; + } + + @computed('selectionDatacenter', 'nodes') get optionsDatacenter() { - // eslint-disable-next-line ember/no-incorrect-calls-with-inline-anonymous-functions + const datacenters = Array.from(new Set(this.nodes.mapBy('datacenter'))).compact(); + + // Update query param when the list of datacenters changes. scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects - this.set( - this.qpDatacenter, - serialize(intersection(this.job.datacenters, this.selectionDatacenter)) - ); + this.set('qpDatacenter', serialize(intersection(datacenters, this.selectionDatacenter))); }); - return this.job.datacenters.sort().map(dc => ({ key: dc, label: dc })); + return datacenters.sort().map(dc => ({ key: dc, label: dc })); } @computed('selectionNodeClass', 'nodes') get optionsNodeClass() { - const nodeClasses = Array.from(new Set(this.nodes.mapBy('nodeClass'))); - // // eslint-disable-next-line ember/no-incorrect-calls-with-inline-anonymous-functions - // scheduleOnce('actions', () => { - // // eslint-disable-next-line ember/no-side-effects - // this.set(this.qpNodeClass, serialize(intersection(nodeClasses, this.selectionNodeClass))); - // }); + const nodeClasses = Array.from(new Set(this.nodes.mapBy('nodeClass'))).compact(); + + // Update query param when the list of datacenters changes. + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set('qpNodeClass', serialize(intersection(nodeClasses, this.selectionNodeClass))); + }); return nodeClasses.sort().map(nodeClass => ({ key: nodeClass, label: nodeClass })); } @@ -142,3 +168,23 @@ export default class ClientsController extends Controller.extend( this.set(queryParam, serialize(selection)); } } + +function eldestCreateTime(allocations) { + let eldest = null; + for (const alloc of allocations) { + if (!eldest || alloc.createTime < eldest) { + eldest = alloc.createTime; + } + } + return eldest; +} + +function mostRecentModifyTime(allocations) { + let mostRecent = null; + for (const alloc of allocations) { + if (!mostRecent || alloc.modifyTime > mostRecent) { + mostRecent = alloc.modifyTime; + } + } + return mostRecent; +} diff --git a/ui/app/routes/jobs/job/clients.js b/ui/app/routes/jobs/job/clients.js index 0be962399834..71b9d23e869a 100644 --- a/ui/app/routes/jobs/job/clients.js +++ b/ui/app/routes/jobs/job/clients.js @@ -1,5 +1,30 @@ import Route from '@ember/routing/route'; +import WithWatchers from 'nomad-ui/mixins/with-watchers'; +import { watchRecord, watchRelationship, watchAll } from 'nomad-ui/utils/properties/watch'; +import { collect } from '@ember/object/computed'; -export default class ClientsRoute extends Route { - // TODO: add watcher for nodes. +export default class ClientsRoute extends Route.extend(WithWatchers) { + async model() { + await this.store.findAll('node'); + return this.modelFor('jobs.job'); + } + + startWatchers(controller, model) { + if (!model) { + return; + } + + controller.set('watchers', { + model: this.watch.perform(model), + allocations: this.watchAllocations.perform(model), + nodes: this.watchNodes.perform(), + }); + } + + @watchRecord('job') watch; + @watchAll('node') watchNodes; + @watchRelationship('allocations') watchAllocations; + + @collect('watch', 'watchNodes', 'watchAllocations') + watchers; } diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index 207f03bcca75..51f796b3a8b4 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -21,6 +21,7 @@ @import './components/gutter-toggle'; @import './components/image-file.scss'; @import './components/inline-definitions'; +@import './components/job-client-status-row'; @import './components/job-diff'; @import './components/json-viewer'; @import './components/legend'; diff --git a/ui/app/styles/components/job-client-status-row.scss b/ui/app/styles/components/job-client-status-row.scss new file mode 100644 index 000000000000..6d93d3f66ab4 --- /dev/null +++ b/ui/app/styles/components/job-client-status-row.scss @@ -0,0 +1,9 @@ +.job-client-status-row { + .allocation-summary { + .is-empty { + color: darken($grey-blue, 20%); + text-align: center; + font-style: italic; + } + } +} diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index e94d8863673b..4e4a347a5ad7 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -15,8 +15,8 @@
    @@ -51,20 +51,19 @@ @class="with-foot" as |t| > - Client ID - + Client Name - + Created - + Modified - + Job Status @@ -74,11 +73,8 @@ diff --git a/ui/mirage/scenarios/sysbatch.js b/ui/mirage/scenarios/sysbatch.js index 7905585e21fc..605861369512 100644 --- a/ui/mirage/scenarios/sysbatch.js +++ b/ui/mirage/scenarios/sysbatch.js @@ -5,6 +5,12 @@ export function sysbatchSmall(server) { status: 'ready', }); + // Create some clients not targeted by the sysbatch job. + server.createList('node', 3, { + datacenter: 'dc3', + status: 'ready', + }); + // Job with 1 task group. const job1 = server.create('job', { status: 'running', From ce3c5174335d8b37ed1495b173691b90ef6ff884 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 15 Sep 2021 13:54:30 -0400 Subject: [PATCH 15/58] client tab test --- ui/app/components/client-status-bar.js | 8 +- ui/app/controllers/jobs/job/clients.js | 4 +- ui/config/environment.js | 2 +- ui/tests/acceptance/job-detail-test.js | 4 + ui/tests/helpers/module-for-job.js | 51 ++- ui/tests/pages/jobs/detail.js | 10 + ui/tests/unit/utils/job-client-status-test.js | 325 +++++++++++++++++- 7 files changed, 383 insertions(+), 21 deletions(-) diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js index 937da6c78c08..6d9fbeb1b819 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/client-status-bar.js @@ -12,7 +12,13 @@ export default class ClientStatusBar extends DistributionBar { // Provide an action with access to the router gotoClient() {} - didUpdateAttrs() { + didRender() { + // append data-test attribute to test link to pre-filtered client tab view + this.element.querySelectorAll('.bars > g').forEach(g => { + g.setAttribute(`data-test-client-status-${g.className.baseVal}`, g.className.baseVal); + }); + + // append on click event to bar chart const { _data, chart } = this; const filteredData = _data.filter(d => d.value > 0); filteredData.forEach((d, index) => { diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index 552813c0a28f..06867b300e78 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -81,7 +81,9 @@ export default class ClientsController extends Controller.extend( @alias('listSearched') sortedClients; get nodes() { - return this.store.peekAll('node'); + return this.store.peekAll('node').length + ? this.store.peekAll('node') + : this.store.findAll('node'); } @computed('nodes', 'selectionStatus', 'selectionDatacenter', 'selectionNodeClass') diff --git a/ui/config/environment.js b/ui/config/environment.js index d5ea567f5a93..fd9caf529fdd 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -26,7 +26,7 @@ module.exports = function(environment) { APP: { blockingQueries: true, // TODO: revert before merging to main. - mirageScenario: 'topoMedium', // convert to 'sysbatchSmall' when working on feature + mirageScenario: 'sysbatchSmall', // convert to 'sysbatchSmall' when working on feature mirageWithNamespaces: false, mirageWithTokens: true, mirageWithRegions: true, diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index c9a0821507ad..38ce795c2b3d 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -14,6 +14,10 @@ moduleForJob('Acceptance | job detail (batch)', 'allocations', () => moduleForJob('Acceptance | job detail (system)', 'allocations', () => server.create('job', { type: 'system', shallow: true }) ); +moduleForJob('Acceptance | job detail (sysbatch)', 'sysbatch', () => + server.create('job', { type: 'sysbatch', shallow: true }) +); + moduleForJob( 'Acceptance | job detail (periodic)', 'children', diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 3a6c27b60eab..ba68378e228e 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -1,4 +1,4 @@ -import { currentURL } from '@ember/test-helpers'; +import { click, currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -12,17 +12,36 @@ export default function moduleForJob(title, context, jobFactory, additionalTests setupApplicationTest(hooks); setupMirage(hooks); hooks.before(function() { - if (context !== 'allocations' && context !== 'children') { + if (context !== 'allocations' && context !== 'children' && context !== 'sysbatch') { throw new Error( - `Invalid context provided to moduleForJob, expected either "allocations" or "children", got ${context}` + `Invalid context provided to moduleForJob, expected either "allocations", "sysbatch" or "children", got ${context}` ); } }); hooks.beforeEach(async function() { - server.create('node'); - job = jobFactory(); - await JobDetail.visit({ id: job.id }); + if (context === 'sysbatch') { + const clients = server.createList('node', 12, { + datacenter: 'dc1', + status: 'ready', + }); + // Job with 1 task group. + job = server.create('job', { + status: 'running', + datacenters: ['dc1', 'dc2'], + type: 'sysbatch', + resourceSpec: ['M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach(c => { + server.create('allocation', { jobId: job.id, nodeId: c.id }); + }); + await JobDetail.visit({ id: job.id }); + } else { + server.create('node'); + job = jobFactory(); + await JobDetail.visit({ id: job.id }); + } }); test('visiting /jobs/:job_id', async function(assert) { @@ -62,6 +81,26 @@ export default function moduleForJob(title, context, jobFactory, additionalTests } }); + if (context === 'sysbatch') { + test('clients for the job are showing in the overview', async function(assert) { + assert.ok( + JobDetail.clientSummary.isPresent, + 'Client Summary Status Bar Chart is displayed in summary section' + ); + }); + test('clicking a status bar in the chart takes you to a pre-filtered view of clients', async function(assert) { + const bars = document.querySelectorAll('[data-test-client-status-bar] > svg > g > g'); + const status = bars[0].className.baseVal; + await click(`[data-test-client-status-${status}="${status}"]`); + const encodedStatus = statusList => encodeURIComponent(JSON.stringify(statusList)); + assert.equal( + currentURL(), + `/jobs/${job.name}/clients?status=${encodedStatus([status])}`, + 'Client Status Bar Chart links to client tab' + ); + }); + } + if (context === 'allocations') { test('allocations for the job are shown in the overview', async function(assert) { assert.ok(JobDetail.allocationsSummary, 'Allocations are shown in the summary section'); diff --git a/ui/tests/pages/jobs/detail.js b/ui/tests/pages/jobs/detail.js index 81f18f849ca9..3d86d422e7e4 100644 --- a/ui/tests/pages/jobs/detail.js +++ b/ui/tests/pages/jobs/detail.js @@ -48,6 +48,8 @@ export default create({ isDisabled: property('disabled'), }, + barChart: isPresent('data-test-client-status-bar'), + stats: collection('[data-test-job-stat]', { id: attribute('data-test-job-stat'), text: text(), @@ -88,4 +90,12 @@ export default create({ recentAllocationsEmptyState: { headline: text('[data-test-empty-recent-allocations-headline]'), }, + + clientSummary: { + id: attribute('[data-test-client-status-bar]'), + }, + visitClients: function(attr) { + console.log('runs\n\n', attr); + clickable(attr); + }, }); diff --git a/ui/tests/unit/utils/job-client-status-test.js b/ui/tests/unit/utils/job-client-status-test.js index c2fa20e530b2..6afdd6f89edb 100644 --- a/ui/tests/unit/utils/job-client-status-test.js +++ b/ui/tests/unit/utils/job-client-status-test.js @@ -1,18 +1,319 @@ import { module, test } from 'qunit'; -import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; -import { jobClientStatus } from 'nomad-ui/utils/job-client-status'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; +import EmberObject from '@ember/object'; -module('Unit | Util | jobclientstatus', function(hooks) { - hooks.beforeEach(async function() { - console.log('hi'); - this.server = startMirage(); +class JobClientStatusMock extends EmberObject { + constructor(job, nodes) { + super(...arguments); + this.job = job; + this.nodes = nodes; + } + + @jobClientStatus('nodes', 'job') jobClientStatus; + + get(key) { + switch (key) { + case 'job': + return this.job; + case 'nodes': + return this.nodes; + } + } +} + +class NodeMock { + constructor(id, datacenter) { + this.id = id; + this.datacenter = datacenter; + } + + get(key) { + switch (key) { + case 'id': + return this.id; + } + } +} + +module('Unit | Util | JobClientStatus', function(hooks) { + test('it handles the case where all nodes are running', async function(assert) { + const node = new NodeMock('node-1', 'dc1'); + const nodes = [node]; + const job = { + datacenters: ['dc1'], + status: 'running', + allocations: [{ node, clientStatus: 'running' }], + taskGroups: [{}], + }; + const expected = { + byNode: { + 'node-1': 'running', + }, + byStatus: { + running: ['node-1'], + complete: [], + degraded: [], + failed: [], + lost: [], + notScheduled: [], + queued: [], + starting: [], + }, + totalNodes: 1, + }; + + const mock = new JobClientStatusMock(job, nodes); + let result = mock.jobClientStatus; + + assert.deepEqual(result, expected); }); - hooks.afterEach(async function() { - this.server.shutdown(); + + test('it handles the degraded case where a node has a failing allocation', async function(assert) { + const node = new NodeMock('node-2', 'dc1'); + const nodes = [node]; + const job = { + datacenters: ['dc1'], + status: 'running', + allocations: [ + { node, clientStatus: 'running' }, + { node, clientStatus: 'failed' }, + { node, clientStatus: 'running' }, + ], + taskGroups: [{}, {}, {}], + }; + const expected = { + byNode: { + 'node-2': 'degraded', + }, + byStatus: { + running: [], + complete: [], + degraded: ['node-2'], + failed: [], + lost: [], + notScheduled: [], + queued: [], + starting: [], + }, + totalNodes: 1, + }; + + const mock = new JobClientStatusMock(job, nodes); + let result = mock.jobClientStatus; + + assert.deepEqual(result, expected); + }); + + test('it handles the case where a node has all lost allocations', async function(assert) { + const node = new NodeMock('node-1', 'dc1'); + const nodes = [node]; + const job = { + datacenters: ['dc1'], + status: 'running', + allocations: [ + { node, clientStatus: 'lost' }, + { node, clientStatus: 'lost' }, + { node, clientStatus: 'lost' }, + ], + taskGroups: [{}, {}, {}], + }; + const expected = { + byNode: { + 'node-1': 'lost', + }, + byStatus: { + running: [], + complete: [], + degraded: [], + failed: [], + lost: ['node-1'], + notScheduled: [], + queued: [], + starting: [], + }, + totalNodes: 1, + }; + + const mock = new JobClientStatusMock(job, nodes); + let result = mock.jobClientStatus; + + assert.deepEqual(result, expected); }); - test('some test', async function(assert) { - // this.pauseTest(); - // jobClientStatus('node', 'job'); - assert.equal(0, 0); + + test('it handles the case where a node has all failed allocations', async function(assert) { + const node = new NodeMock('node-1', 'dc1'); + const nodes = [node]; + const job = { + datacenters: ['dc1'], + status: 'running', + allocations: [ + { node, clientStatus: 'failed' }, + { node, clientStatus: 'failed' }, + { node, clientStatus: 'failed' }, + ], + taskGroups: [{}, {}, {}], + }; + const expected = { + byNode: { + 'node-1': 'failed', + }, + byStatus: { + running: [], + complete: [], + degraded: [], + failed: ['node-1'], + lost: [], + notScheduled: [], + queued: [], + starting: [], + }, + totalNodes: 1, + }; + + const mock = new JobClientStatusMock(job, nodes); + let result = mock.jobClientStatus; + + assert.deepEqual(result, expected); + }); + + test('it handles the degraded case where the expected number of allocations doesnt match the actual number of allocations', async function(assert) { + const node = new NodeMock('node-1', 'dc1'); + const nodes = [node]; + const job = { + datacenters: ['dc1'], + status: 'running', + allocations: [ + { node, clientStatus: 'running' }, + { node, clientStatus: 'running' }, + { node, clientStatus: 'running' }, + ], + taskGroups: [{}, {}, {}, {}], + }; + const expected = { + byNode: { + 'node-1': 'degraded', + }, + byStatus: { + running: [], + complete: [], + degraded: ['node-1'], + failed: [], + lost: [], + notScheduled: [], + queued: [], + starting: [], + }, + totalNodes: 1, + }; + + const mock = new JobClientStatusMock(job, nodes); + let result = mock.jobClientStatus; + + assert.deepEqual(result, expected); + }); + + test('it handles the not scheduled case where a node has no allocations', async function(assert) { + const node = new NodeMock('node-1', 'dc1'); + const nodes = [node]; + const job = { + datacenters: ['dc1'], + status: 'running', + allocations: [], + taskGroups: [], + }; + const expected = { + byNode: { + 'node-1': 'notScheduled', + }, + byStatus: { + running: [], + complete: [], + degraded: [], + failed: [], + lost: [], + notScheduled: ['node-1'], + queued: [], + starting: [], + }, + totalNodes: 1, + }; + + const mock = new JobClientStatusMock(job, nodes); + let result = mock.jobClientStatus; + + assert.deepEqual(result, expected); + }); + + test('it handles the queued case where the job is pending', async function(assert) { + const node = new NodeMock('node-1', 'dc1'); + const nodes = [node]; + const job = { + datacenters: ['dc1'], + status: 'pending', + allocations: [ + { node, clientStatus: 'starting' }, // technically shouldn't be possible but testing the logic + { node, clientStatus: 'starting' }, + { node, clientStatus: 'starting' }, + ], + taskGroups: [{}, {}, {}, {}], + }; + const expected = { + byNode: { + 'node-1': 'queued', + }, + byStatus: { + running: [], + complete: [], + degraded: [], + failed: [], + lost: [], + notScheduled: [], + queued: ['node-1'], + starting: [], + }, + totalNodes: 1, + }; + + const mock = new JobClientStatusMock(job, nodes); + let result = mock.jobClientStatus; + + assert.deepEqual(result, expected); + }); + + test('it filters nodes by the datacenter of the job', async function(assert) { + const node1 = new NodeMock('node-1', 'dc1'); + const node2 = new NodeMock('node-2', 'dc2'); + const nodes = [node1, node2]; + const job = { + datacenters: ['dc1'], + status: 'running', + allocations: [ + { node: node1, clientStatus: 'running' }, + { node: node2, clientStatus: 'failed' }, + { node: node1, clientStatus: 'running' }, + ], + taskGroups: [{}, {}], + }; + const expected = { + byNode: { + 'node-1': 'running', + }, + byStatus: { + running: ['node-1'], + complete: [], + degraded: [], + failed: [], + lost: [], + notScheduled: [], + queued: [], + starting: [], + }, + totalNodes: 1, + }; + + const mock = new JobClientStatusMock(job, nodes); + let result = mock.jobClientStatus; + + assert.deepEqual(result, expected); }); }); From 749828f623b749136d27ee74f215b1371e99e927 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 15 Sep 2021 13:57:03 -0400 Subject: [PATCH 16/58] client tab test --- ui/tests/acceptance/job-clients-test.js | 42 +++++++++++++++++++++++++ ui/tests/pages/jobs/job/clients.js | 42 +++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 ui/tests/acceptance/job-clients-test.js create mode 100644 ui/tests/pages/jobs/job/clients.js diff --git a/ui/tests/acceptance/job-clients-test.js b/ui/tests/acceptance/job-clients-test.js new file mode 100644 index 000000000000..d312a861c5b6 --- /dev/null +++ b/ui/tests/acceptance/job-clients-test.js @@ -0,0 +1,42 @@ +/* eslint-disable */ +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; +import Clients from 'nomad-ui/tests/pages/jobs/job/clients'; + +let job; +module('Acceptance | job clients', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function() { + server.create('node'); + + const clients = server.createList('node', 12, { + datacenter: 'dc1', + status: 'ready', + }); + // Job with 1 task group. + job = server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'sysbatch', + resourceSpec: ['M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach(c => { + server.create('allocation', { jobId: job.id, nodeId: c.id }); + }); + console.log( + 'mirage nodes\n\n\n', + clients.map(c => c.id) + ); + }); + + test('it passes an accessibility audit', async function(assert) { + await Clients.visit({ id: job.id }); + await this.pauseTest(); + await a11yAudit(assert); + }); +}); diff --git a/ui/tests/pages/jobs/job/clients.js b/ui/tests/pages/jobs/job/clients.js new file mode 100644 index 000000000000..52cc0f442852 --- /dev/null +++ b/ui/tests/pages/jobs/job/clients.js @@ -0,0 +1,42 @@ +import { + attribute, + clickable, + create, + collection, + fillable, + isPresent, + text, + visitable, +} from 'ember-cli-page-object'; + +// import clients from 'nomad-ui/tests/pages/components/clients'; +import error from 'nomad-ui/tests/pages/components/error'; + +export default create({ + visit: visitable('/jobs/:id/clients'), + pageSize: 25, + + hasSearchBox: isPresent('[data-test-allocations-search]'), + search: fillable('[data-test-allocations-search] input'), + + // ...clients(), + + isEmpty: isPresent('[data-test-empty-allocations-list]'), + emptyState: { + headline: text('[data-test-empty-allocations-list-headline]'), + }, + + sortOptions: collection('[data-test-sort-by]', { + id: attribute('data-test-sort-by'), + sort: clickable(), + }), + + sortBy(id) { + return this.sortOptions + .toArray() + .findBy('id', id) + .sort(); + }, + + error: error(), +}); From 7ad3e300f206b2a9939c836d32f2e4d75c570bf6 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 15 Sep 2021 14:10:12 -0400 Subject: [PATCH 17/58] lint fixes --- ui/app/components/client-status-bar.js | 28 +++++++++++++------ ui/app/components/job-page/parts/summary.js | 2 +- ui/app/controllers/jobs/job/clients.js | 4 +-- ui/tests/unit/utils/job-client-status-test.js | 1 - 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js index 937da6c78c08..792987143541 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/client-status-bar.js @@ -28,48 +28,58 @@ export default class ClientStatusBar extends DistributionBar { }); } - @computed('jobClientStatus') + @computed('jobClientStatus.byStatus') get data() { + const { + queued, + starting, + running, + complete, + degraded, + failed, + lost, + notScheduled, + } = this.jobClientStatus.byStatus; return [ { label: 'Queued', - value: this.jobClientStatus.byStatus.queued.length, + value: queued.length, className: 'queued', }, { label: 'Starting', - value: this.jobClientStatus.byStatus.starting.length, + value: starting.length, className: 'starting', layers: 2, }, { label: 'Running', - value: this.jobClientStatus.byStatus.running.length, + value: running.length, className: 'running', }, { label: 'Complete', - value: this.jobClientStatus.byStatus.complete.length, + value: complete.length, className: 'complete', }, { label: 'Degraded', - value: this.jobClientStatus.byStatus.degraded.length, + value: degraded.length, className: 'degraded', }, { label: 'Failed', - value: this.jobClientStatus.byStatus.failed.length, + value: failed.length, className: 'failed', }, { label: 'Lost', - value: this.jobClientStatus.byStatus.lost.length, + value: lost.length, className: 'lost', }, { label: 'Not Scheduled', - value: this.jobClientStatus.byStatus.notScheduled.length, + value: notScheduled.length, className: 'not-scheduled', }, ]; diff --git a/ui/app/components/job-page/parts/summary.js b/ui/app/components/job-page/parts/summary.js index f42b909e2461..0cb821cc8344 100644 --- a/ui/app/components/job-page/parts/summary.js +++ b/ui/app/components/job-page/parts/summary.js @@ -9,7 +9,7 @@ export default class Summary extends Component { job = null; forceCollapsed = false; - @computed + @computed('forceCollapsed') get isExpanded() { if (this.forceCollapsed) return false; diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index ad46d9d6e5d5..7c9ee3799ad8 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -62,12 +62,12 @@ export default class ClientsController extends Controller.extend( @alias('listSorted') listToSearch; @alias('listSearched') sortedClients; - @computed + @computed('store') get allNodes() { return this.store.peekAll('node'); } - @computed('allNodes') + @computed('allNodes', 'jobClientStatus.byNode') get nodes() { return this.allNodes.filter(node => this.jobClientStatus.byNode[node.id]); } diff --git a/ui/tests/unit/utils/job-client-status-test.js b/ui/tests/unit/utils/job-client-status-test.js index c2fa20e530b2..338f946dc924 100644 --- a/ui/tests/unit/utils/job-client-status-test.js +++ b/ui/tests/unit/utils/job-client-status-test.js @@ -1,6 +1,5 @@ import { module, test } from 'qunit'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; -import { jobClientStatus } from 'nomad-ui/utils/job-client-status'; module('Unit | Util | jobclientstatus', function(hooks) { hooks.beforeEach(async function() { From d4d6a8a094d04bad4c96d355edec4907b77e7097 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 15 Sep 2021 14:11:42 -0400 Subject: [PATCH 18/58] get all nodes if client tab is first page we land on --- ui/app/controllers/jobs/job/clients.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index 7c9ee3799ad8..63e1da8251ee 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -64,7 +64,9 @@ export default class ClientsController extends Controller.extend( @computed('store') get allNodes() { - return this.store.peekAll('node'); + return this.store.peekAll('node').length + ? this.store.peekAll('node') + : this.store.findAll('node'); } @computed('allNodes', 'jobClientStatus.byNode') From 8f711a471275ce8a1300d552e8b5379fe4a87e97 Mon Sep 17 00:00:00 2001 From: Jai <41024828+JBhagat841@users.noreply.github.com> Date: Wed, 15 Sep 2021 14:30:38 -0400 Subject: [PATCH 19/58] add whitespace --- ui/app/controllers/jobs/job/clients.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index 63e1da8251ee..d357dfe72bc2 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -13,10 +13,10 @@ import classic from 'ember-classic-decorator'; @classic export default class ClientsController extends Controller.extend( - SortableFactory(['id', 'name', 'jobStatus']), - Searchable, - WithNamespaceResetting -) { + SortableFactory(['id', 'name', 'jobStatus']), + Searchable, + WithNamespaceResetting + ) { queryParams = [ { currentPage: 'page', From 0453eaf91549b21ca35e9a2486ceaa22aa26c950 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Thu, 16 Sep 2021 17:00:30 -0400 Subject: [PATCH 20/58] integration test for component --- ui/app/components/client-status-bar.js | 10 +--- .../job-page/parts/job-client-summary.hbs | 2 +- ui/tests/acceptance/job-clients-test.js | 8 --- .../components/client-status-bar-test.js | 55 +++++++++++++++++++ .../pages/components/client-status-bar.js | 16 ++++++ 5 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 ui/tests/integration/components/client-status-bar-test.js create mode 100644 ui/tests/pages/components/client-status-bar.js diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js index e8b5387a03de..9ca2748b74d0 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/client-status-bar.js @@ -10,14 +10,9 @@ export default class ClientStatusBar extends DistributionBar { jobClientStatus = null; // Provide an action with access to the router - gotoClient() {} + onBarClick() {} didRender() { - // append data-test attribute to test link to pre-filtered client tab view - this.element.querySelectorAll('.bars > g').forEach(g => { - g.setAttribute(`data-test-client-status-${g.className.baseVal}`, g.className.baseVal); - }); - // append on click event to bar chart const { _data, chart } = this; const filteredData = _data.filter(d => d.value > 0); @@ -28,9 +23,10 @@ export default class ClientStatusBar extends DistributionBar { .select('.bars') .selectAll('g') .data(filteredData, d => d.label) + .attr('data-test-client-status', d => d.label.toLowerCase()) .on('click', d => { let label = d.label === 'Not Scheduled' ? 'notScheduled' : d.label; - this.gotoClient(label); + this.onBarClick(label); }); } diff --git a/ui/app/templates/components/job-page/parts/job-client-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-summary.hbs index c68e485cf74c..602e7bfecc84 100644 --- a/ui/app/templates/components/job-page/parts/job-client-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-summary.hbs @@ -24,7 +24,7 @@ diff --git a/ui/tests/acceptance/job-clients-test.js b/ui/tests/acceptance/job-clients-test.js index d312a861c5b6..ff207509d903 100644 --- a/ui/tests/acceptance/job-clients-test.js +++ b/ui/tests/acceptance/job-clients-test.js @@ -1,4 +1,3 @@ -/* eslint-disable */ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -11,8 +10,6 @@ module('Acceptance | job clients', function(hooks) { setupMirage(hooks); hooks.beforeEach(function() { - server.create('node'); - const clients = server.createList('node', 12, { datacenter: 'dc1', status: 'ready', @@ -28,15 +25,10 @@ module('Acceptance | job clients', function(hooks) { clients.forEach(c => { server.create('allocation', { jobId: job.id, nodeId: c.id }); }); - console.log( - 'mirage nodes\n\n\n', - clients.map(c => c.id) - ); }); test('it passes an accessibility audit', async function(assert) { await Clients.visit({ id: job.id }); - await this.pauseTest(); await a11yAudit(assert); }); }); diff --git a/ui/tests/integration/components/client-status-bar-test.js b/ui/tests/integration/components/client-status-bar-test.js new file mode 100644 index 000000000000..5f4cbddea55b --- /dev/null +++ b/ui/tests/integration/components/client-status-bar-test.js @@ -0,0 +1,55 @@ +import { module, test } from 'qunit'; +import { create } from 'ember-cli-page-object'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; +import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; +import clientStatusBar from 'nomad-ui/tests/pages/components/client-status-bar'; + +const ClientStatusBar = create(clientStatusBar()); + +module('Integration | Component | client-status-bar', function(hooks) { + setupRenderingTest(hooks); + + const commonProperties = () => ({ + onBarClick: sinon.spy(), + jobClientStatus: { + byStatus: { + queued: [], + starting: ['someNodeId'], + running: [], + complete: [], + degraded: [], + failed: [], + lost: [], + notScheduled: [], + }, + }, + isNarrow: true, + }); + + const commonTemplate = hbs` + `; + + test('it renders', async function(assert) { + const props = commonProperties(); + this.setProperties(props); + await render(commonTemplate); + + assert.ok(ClientStatusBar.isPresent, 'Client Status Bar is rendered'); + await componentA11yAudit(this.element, assert); + }); + + test('it fires the onBarClick handler method when clicking a bar in the chart', async function(assert) { + const props = commonProperties(); + this.setProperties(props); + await render(commonTemplate); + await ClientStatusBar.visitBar('starting'); + assert.ok(props.onBarClick.calledOnce); + }); +}); diff --git a/ui/tests/pages/components/client-status-bar.js b/ui/tests/pages/components/client-status-bar.js new file mode 100644 index 000000000000..244e63c986fd --- /dev/null +++ b/ui/tests/pages/components/client-status-bar.js @@ -0,0 +1,16 @@ +import { attribute, clickable, collection } from 'ember-cli-page-object'; + +export default scope => ({ + scope, + + bars: collection('.bars > g', { + id: attribute('data-test-client-status'), + visit: clickable(), + }), + + visitBar: async function(id) { + const bar = this.bars.toArray().findBy('id', id); + console.log('this.bars\n\n', bar.id); + await bar.visit(); + }, +}); From c57db19b733be7722182a42eac4be57b5c883e43 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Fri, 17 Sep 2021 14:36:38 -0400 Subject: [PATCH 21/58] acceptance tests for client tab --- ui/app/templates/jobs/job/clients.hbs | 2 +- ui/tests/acceptance/job-clients-test.js | 94 ++++++++++++++++++- .../components/client-status-bar-test.js | 16 ++++ .../pages/components/client-status-bar.js | 1 - ui/tests/pages/components/clients.js | 29 ++++++ ui/tests/pages/jobs/job/clients.js | 12 +-- 6 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 ui/tests/pages/components/clients.js diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index 4e4a347a5ad7..230634fc678f 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -72,7 +72,7 @@ diff --git a/ui/tests/acceptance/job-clients-test.js b/ui/tests/acceptance/job-clients-test.js index ff207509d903..e613f788d9ac 100644 --- a/ui/tests/acceptance/job-clients-test.js +++ b/ui/tests/acceptance/job-clients-test.js @@ -1,3 +1,4 @@ +import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -5,12 +6,25 @@ import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; import Clients from 'nomad-ui/tests/pages/jobs/job/clients'; let job; +let clients; + +const makeSearchableClients = server => { + Array(10) + .fill(null) + .map((_, index) => { + server.create('node', { + id: index < 5 ? `ffffff-dddddd-${index}` : `111111-222222-${index}`, + shallow: true, + }); + }); +}; + module('Acceptance | job clients', function(hooks) { setupApplicationTest(hooks); setupMirage(hooks); hooks.beforeEach(function() { - const clients = server.createList('node', 12, { + clients = server.createList('node', 12, { datacenter: 'dc1', status: 'ready', }); @@ -31,4 +45,82 @@ module('Acceptance | job clients', function(hooks) { await Clients.visit({ id: job.id }); await a11yAudit(assert); }); + + test('lists all clients for the job', async function(assert) { + await Clients.visit({ id: job.id }); + assert.equal(Clients.clients.length, 12, 'Clients are shown in a table'); + + const sortedClients = clients; + + Clients.clients.forEach((client, index) => { + const shortId = sortedClients[index].id.split('-')[0]; + console.log('client\n\n', client); + assert.equal(client.shortId, shortId, `Client ${index} is ${shortId}`); + }); + + assert.equal(document.title, `Job ${job.name} clients - Nomad`); + }); + + test('clients table is sortable', async function(assert) { + await Clients.visit({ id: job.id }); + await Clients.sortBy('modifyTime'); + + assert.equal( + currentURL(), + `/jobs/${job.id}/clients?desc=true&sort=modifyTime`, + 'the URL persists the sort parameter' + ); + const sortedClients = clients.sortBy('modifyTime').reverse(); + Clients.clients.forEach((client, index) => { + const shortId = sortedClients[index].id.split('-')[0]; + assert.equal( + client.shortId, + shortId, + `Client ${index} is ${shortId} with modify time ${sortedClients[index].modifyTime}` + ); + }); + }); + + test('clients table is searchable', async function(assert) { + makeSearchableClients(server); + + clients = server.schema.nodes.where({ jobId: job.id }).models; + + await Clients.visit({ id: job.id }); + await Clients.search('ffffff'); + + assert.equal(Clients.clients.length, 5, 'List is filtered by search term'); + }); + + test('when a search yields no results, the search box remains', async function(assert) { + makeSearchableClients(server); + + clients = server.schema.nodes.where({ jobId: job.id }).models; + + await Clients.visit({ id: job.id }); + await Clients.search('^nothing will ever match this long regex$'); + + assert.equal( + Clients.emptyState.headline, + 'No Matches', + 'List is empty and the empty state is about search' + ); + + assert.ok(Clients.hasSearchBox, 'Search box is still shown'); + }); + + test('when the job for the clients is not found, an error message is shown, but the URL persists', async function(assert) { + await Clients.visit({ id: 'not-a-real-job' }); + + assert.equal( + server.pretender.handledRequests + .filter(request => !request.url.includes('policy')) + .findBy('status', 404).url, + '/v1/job/not-a-real-job', + 'A request to the nonexistent job is made' + ); + assert.equal(currentURL(), '/jobs/not-a-real-job/clients', 'The URL persists'); + assert.ok(Clients.error.isPresent, 'Error message is shown'); + assert.equal(Clients.error.title, 'Not Found', 'Error message is for 404'); + }); }); diff --git a/ui/tests/integration/components/client-status-bar-test.js b/ui/tests/integration/components/client-status-bar-test.js index 5f4cbddea55b..948a3ff07ed1 100644 --- a/ui/tests/integration/components/client-status-bar-test.js +++ b/ui/tests/integration/components/client-status-bar-test.js @@ -52,4 +52,20 @@ module('Integration | Component | client-status-bar', function(hooks) { await ClientStatusBar.visitBar('starting'); assert.ok(props.onBarClick.calledOnce); }); + + test('it handles an update to client status property', async function(assert) { + const props = commonProperties(); + this.setProperties(props); + await render(commonTemplate); + const newProps = { + ...props, + jobClientStatus: { + ...props.jobClientStatus, + byStatus: { ...props.jobClientStatus.byStatus, starting: [], running: ['someNodeId'] }, + }, + }; + this.setProperties(newProps); + await ClientStatusBar.visitBar('running'); + assert.ok(props.onBarClick.calledOnce); + }); }); diff --git a/ui/tests/pages/components/client-status-bar.js b/ui/tests/pages/components/client-status-bar.js index 244e63c986fd..46e95de6d1f9 100644 --- a/ui/tests/pages/components/client-status-bar.js +++ b/ui/tests/pages/components/client-status-bar.js @@ -10,7 +10,6 @@ export default scope => ({ visitBar: async function(id) { const bar = this.bars.toArray().findBy('id', id); - console.log('this.bars\n\n', bar.id); await bar.visit(); }, }); diff --git a/ui/tests/pages/components/clients.js b/ui/tests/pages/components/clients.js new file mode 100644 index 000000000000..649802977832 --- /dev/null +++ b/ui/tests/pages/components/clients.js @@ -0,0 +1,29 @@ +import { attribute, collection, clickable, text } from 'ember-cli-page-object'; +import { singularize } from 'ember-inflector'; + +export default function(selector = '[data-test-client]', propKey = 'clients') { + const lookupKey = `${singularize(propKey)}For`; + // Remove the bracket notation + const attr = selector.substring(1, selector.length - 1); + return { + [propKey]: collection(selector, { + id: attribute(attr), + shortId: text('[data-test-short-id]'), + createTime: text('[data-test-create-time]'), + createTooltip: attribute('aria-label', '[data-test-create-time] .tooltip'), + modifyTime: text('[data-test-modify-time]'), + status: text('[data-test-client-status]'), + job: text('[data-test-job]'), + client: text('[data-test-client]'), + + visit: clickable('[data-test-short-id] a'), + visitRow: clickable(), + visitJob: clickable('[data-test-job]'), + visitClient: clickable('[data-test-client] a'), + }), + + [lookupKey]: function(id) { + return this[propKey].toArray().find(client => client.id === id); + }, + }; +} diff --git a/ui/tests/pages/jobs/job/clients.js b/ui/tests/pages/jobs/job/clients.js index 52cc0f442852..8df5401c5b5a 100644 --- a/ui/tests/pages/jobs/job/clients.js +++ b/ui/tests/pages/jobs/job/clients.js @@ -9,21 +9,21 @@ import { visitable, } from 'ember-cli-page-object'; -// import clients from 'nomad-ui/tests/pages/components/clients'; +import clients from 'nomad-ui/tests/pages/components/clients'; import error from 'nomad-ui/tests/pages/components/error'; export default create({ visit: visitable('/jobs/:id/clients'), pageSize: 25, - hasSearchBox: isPresent('[data-test-allocations-search]'), - search: fillable('[data-test-allocations-search] input'), + hasSearchBox: isPresent('[data-test-clients-search]'), + search: fillable('[data-test-clients-search] input'), - // ...clients(), + ...clients(), - isEmpty: isPresent('[data-test-empty-allocations-list]'), + isEmpty: isPresent('[data-test-empty-clients-list]'), emptyState: { - headline: text('[data-test-empty-allocations-list-headline]'), + headline: text('[data-test-empty-clients-list-headline]'), }, sortOptions: collection('[data-test-sort-by]', { From b727f376771a274cd195fa0ca4a6661051b54a18 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 21 Sep 2021 14:19:15 -0400 Subject: [PATCH 22/58] ui: refactor job summary bar click --- ui/app/components/client-status-bar.js | 24 +---------- ui/app/components/distribution-bar.js | 5 +++ .../job-page/parts/job-client-summary.js | 8 +++- ui/app/controllers/jobs/job/index.js | 7 ++-- .../job-page/parts/job-client-summary.hbs | 8 +++- .../components/job-page/sysbatch.hbs | 41 ++++++------------- .../templates/components/job-page/system.hbs | 5 ++- ui/app/templates/jobs/job/index.hbs | 2 +- 8 files changed, 42 insertions(+), 58 deletions(-) diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/client-status-bar.js index 9ca2748b74d0..f9abb95cf146 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/client-status-bar.js @@ -1,4 +1,4 @@ -import { computed, set } from '@ember/object'; +import { computed } from '@ember/object'; import DistributionBar from './distribution-bar'; import classic from 'ember-classic-decorator'; @@ -8,27 +8,7 @@ export default class ClientStatusBar extends DistributionBar { 'data-test-client-status-bar' = true; jobClientStatus = null; - - // Provide an action with access to the router - onBarClick() {} - - didRender() { - // append on click event to bar chart - const { _data, chart } = this; - const filteredData = _data.filter(d => d.value > 0); - filteredData.forEach((d, index) => { - set(d, 'index', index); - }); - chart - .select('.bars') - .selectAll('g') - .data(filteredData, d => d.label) - .attr('data-test-client-status', d => d.label.toLowerCase()) - .on('click', d => { - let label = d.label === 'Not Scheduled' ? 'notScheduled' : d.label; - this.onBarClick(label); - }); - } + onSliceClick() {} @computed('jobClientStatus.byStatus') get data() { diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index ee9af8456d62..286a060ba6f8 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -22,6 +22,7 @@ const sumAggregate = (total, val) => total + val; export default class DistributionBar extends Component.extend(WindowResizable) { chart = null; @overridable(() => null) data; + @overridable(() => null) onSliceClick; activeDatum = null; isNarrow = false; @@ -94,6 +95,10 @@ export default class DistributionBar extends Component.extend(WindowResizable) { slices.exit().remove(); + if (this.onSliceClick) { + slices.on('click', this.onSliceClick); + } + let slicesEnter = slices.enter() .append('g') .on('mouseenter', d => { diff --git a/ui/app/components/job-page/parts/job-client-summary.js b/ui/app/components/job-page/parts/job-client-summary.js index 4e64fea40030..7575ebb6d6cb 100644 --- a/ui/app/components/job-page/parts/job-client-summary.js +++ b/ui/app/components/job-page/parts/job-client-summary.js @@ -1,5 +1,5 @@ import Component from '@ember/component'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import { inject as service } from '@ember/service'; import { classNames } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; @@ -11,6 +11,7 @@ export default class JobClientSummary extends Component { job = null; jobClientStatus = null; + gotoClients() {} @computed get isExpanded() { @@ -18,6 +19,11 @@ export default class JobClientSummary extends Component { return storageValue != null ? JSON.parse(storageValue) : true; } + @action + onSliceClick(slice) { + this.gotoClients([slice.className.camelize()]); + } + persist(item, isOpen) { window.localStorage.nomadExpandJobClientSummary = isOpen; this.notifyPropertyChange('isExpanded'); diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 6c580336a5e2..3bbcdeed184e 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -41,8 +41,9 @@ export default class IndexController extends Controller.extend(WithNamespaceRese } @action - gotoClient(d) { - const encodeStatus = JSON.stringify([`${d.charAt(0).toLowerCase()}${d.slice(1)}`]); - this.transitionToRoute('jobs.job.clients', this.job, { queryParams: { status: encodeStatus } }); + gotoClients(statusFilter) { + this.transitionToRoute('jobs.job.clients', this.job, { + queryParams: { status: JSON.stringify(statusFilter) }, + }); } } diff --git a/ui/app/templates/components/job-page/parts/job-client-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-summary.hbs index 602e7bfecc84..5c55da7ff5b2 100644 --- a/ui/app/templates/components/job-page/parts/job-client-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-summary.hbs @@ -16,7 +16,11 @@ {{#unless a.isOpen}}
    - +
    {{/unless}} @@ -24,7 +28,7 @@ diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs index d4a7895fd5da..3c18a05aa0db 100644 --- a/ui/app/templates/components/job-page/sysbatch.hbs +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -1,47 +1,32 @@ - + + +
    - - - Type: - - {{this.job.type}} - | - - - - Priority: - - {{this.job.priority}} - + Type:{{this.job.type}} | + Priority:{{this.job.priority}} {{#if (and this.job.namespace this.system.shouldShowNamespaces)}} - - | - - Namespace: - - {{this.job.namespace.name}} - + | Namespace:{{this.job.namespace.name}} {{/if}}
    + + @gotoClients={{this.gotoClients}} /> + + + + @gotoTaskGroup={{this.gotoTaskGroup}} /> +
    \ No newline at end of file diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 66d5db0e43eb..030a41d5f3f0 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -19,7 +19,10 @@ {{/each}} {{/if}} - + diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index 53d3e6ecbf26..92329d5ee205 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -6,6 +6,6 @@ sortDescending=this.sortDescending currentPage=this.currentPage gotoJob=(action "gotoJob") - gotoClient=(action "gotoClient") gotoTaskGroup=(action "gotoTaskGroup") + gotoClients=(action "gotoClients") }} \ No newline at end of file From 27d7a68031fad0ce557c407c5c466b9a9f2eae2e Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 21 Sep 2021 16:43:23 -0400 Subject: [PATCH 23/58] ui: set node watcher only if needed --- ui/app/models/job.js | 5 +++++ ui/app/routes/jobs/job/index.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 922b8a86ed8e..8182546dfde6 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -39,6 +39,11 @@ export default class Job extends Model { return this.periodic || (this.parameterized && !this.dispatched); } + @computed('type') + get hasClientStatus() { + return this.type === 'system' || this.type === 'sysbatch'; + } + @belongsTo('job', { inverse: 'children' }) parent; @hasMany('job', { inverse: 'parent' }) children; diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index 933fcf95ba95..854908bea302 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -22,7 +22,7 @@ export default class IndexRoute extends Route.extend(WithWatchers) { latestDeployment: model.get('supportsDeployments') && this.watchLatestDeployment.perform(model), list: model.get('hasChildren') && this.watchAll.perform(), - nodes: /*model.type === 'sysbatch' && */ this.watchNodes.perform(), + nodes: model.get('hasClientStatus') && this.watchNodes.perform(), }); } From 9687a276e7b1248aeca169c4f5ac9e5aafa6f209 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 21 Sep 2021 16:47:34 -0400 Subject: [PATCH 24/58] ui: rename some files and components --- ...client-status-bar.js => job-client-status-bar.js} | 2 +- .../{client-row.js => job-client-status-row.js} | 0 ...lient-summary.js => job-client-status-summary.js} | 8 +++----- .../components/job-client-status-row.hbs} | 12 ++++++------ ...ent-summary.hbs => job-client-status-summary.hbs} | 6 +++--- ui/app/templates/components/job-page/sysbatch.hbs | 2 +- ui/app/templates/components/job-page/system.hbs | 2 +- ui/app/templates/jobs/job/clients.hbs | 2 +- 8 files changed, 16 insertions(+), 18 deletions(-) rename ui/app/components/{client-status-bar.js => job-client-status-bar.js} (95%) rename ui/app/components/{client-row.js => job-client-status-row.js} (100%) rename ui/app/components/job-page/parts/{job-client-summary.js => job-client-status-summary.js} (82%) rename ui/app/{components/client-row.hbs => templates/components/job-client-status-row.hbs} (77%) rename ui/app/templates/components/job-page/parts/{job-client-summary.hbs => job-client-status-summary.hbs} (95%) diff --git a/ui/app/components/client-status-bar.js b/ui/app/components/job-client-status-bar.js similarity index 95% rename from ui/app/components/client-status-bar.js rename to ui/app/components/job-client-status-bar.js index f9abb95cf146..623eb82d519c 100644 --- a/ui/app/components/client-status-bar.js +++ b/ui/app/components/job-client-status-bar.js @@ -3,7 +3,7 @@ import DistributionBar from './distribution-bar'; import classic from 'ember-classic-decorator'; @classic -export default class ClientStatusBar extends DistributionBar { +export default class JobClientStatusBar extends DistributionBar { layoutName = 'components/distribution-bar'; 'data-test-client-status-bar' = true; diff --git a/ui/app/components/client-row.js b/ui/app/components/job-client-status-row.js similarity index 100% rename from ui/app/components/client-row.js rename to ui/app/components/job-client-status-row.js diff --git a/ui/app/components/job-page/parts/job-client-summary.js b/ui/app/components/job-page/parts/job-client-status-summary.js similarity index 82% rename from ui/app/components/job-page/parts/job-client-summary.js rename to ui/app/components/job-page/parts/job-client-status-summary.js index 7575ebb6d6cb..9faa03b5a150 100644 --- a/ui/app/components/job-page/parts/job-client-summary.js +++ b/ui/app/components/job-page/parts/job-client-status-summary.js @@ -6,16 +6,14 @@ import classic from 'ember-classic-decorator'; @classic @classNames('boxed-section') -export default class JobClientSummary extends Component { - @service store; - +export default class JobClientStatusSummary extends Component { job = null; jobClientStatus = null; gotoClients() {} @computed get isExpanded() { - const storageValue = window.localStorage.nomadExpandJobClientSummary; + const storageValue = window.localStorage.nomadExpandJobClientStatusSummary; return storageValue != null ? JSON.parse(storageValue) : true; } @@ -25,7 +23,7 @@ export default class JobClientSummary extends Component { } persist(item, isOpen) { - window.localStorage.nomadExpandJobClientSummary = isOpen; + window.localStorage.nomadExpandJobClientStatusSummary = isOpen; this.notifyPropertyChange('isExpanded'); } } diff --git a/ui/app/components/client-row.hbs b/ui/app/templates/components/job-client-status-row.hbs similarity index 77% rename from ui/app/components/client-row.hbs rename to ui/app/templates/components/job-client-status-row.hbs index d0094ab1872e..545620cd37e6 100644 --- a/ui/app/components/client-row.hbs +++ b/ui/app/templates/components/job-client-status-row.hbs @@ -7,18 +7,18 @@ {{#if this.row.createTime}} - - {{moment-from-now this.row.createTime}} - + + {{moment-from-now this.row.createTime}} + {{else}} - {{/if}} {{#if this.row.modifyTime}} - - {{moment-from-now this.row.modifyTime}} - + + {{moment-from-now this.row.modifyTime}} + {{else}} - {{/if}} diff --git a/ui/app/templates/components/job-page/parts/job-client-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs similarity index 95% rename from ui/app/templates/components/job-page/parts/job-client-summary.hbs rename to ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index 5c55da7ff5b2..eede8622911f 100644 --- a/ui/app/templates/components/job-page/parts/job-client-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -16,7 +16,7 @@ {{#unless a.isOpen}}
    - - {{/each}}
- +
\ No newline at end of file diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs index 3c18a05aa0db..35f6d6faa3ad 100644 --- a/ui/app/templates/components/job-page/sysbatch.hbs +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -13,7 +13,7 @@ - diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 030a41d5f3f0..8734536b760d 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -19,7 +19,7 @@ {{/each}} {{/if}} - diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index 230634fc678f..dcb74e0ced7a 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -71,7 +71,7 @@ - Date: Tue, 21 Sep 2021 17:27:18 -0400 Subject: [PATCH 25/58] ui: only set pointer cursor if the slice has click event --- ui/app/components/distribution-bar.js | 8 +++++++- ui/app/styles/charts/distribution-bar.scss | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index 286a060ba6f8..16e351eeaa5b 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -126,7 +126,13 @@ export default class DistributionBar extends Component.extend(WindowResizable) { const activeDatum = this.activeDatum; const isActive = activeDatum && activeDatum.label === d.label; const isInactive = activeDatum && activeDatum.label !== d.label; - return [ className, isActive && 'active', isInactive && 'inactive' ].compact().join(' '); + const isClickable = !!this.onSliceClick; + return [ + className, + isActive && 'active', + isInactive && 'inactive', + isClickable && 'clickable' + ].compact().join(' '); }); this.set('slices', slices); diff --git a/ui/app/styles/charts/distribution-bar.scss b/ui/app/styles/charts/distribution-bar.scss index 7620615f43c5..14fa3716652c 100644 --- a/ui/app/styles/charts/distribution-bar.scss +++ b/ui/app/styles/charts/distribution-bar.scss @@ -8,12 +8,15 @@ width: 100%; .bars { - cursor: pointer; rect { transition: opacity 0.3s ease-in-out; opacity: 1; } + .clickable { + cursor: pointer; + } + .inactive { opacity: 0.2; } From 7e7525fc832b2a3a4b69c9cbd1d4fc6ee7057357 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 21 Sep 2021 17:33:28 -0400 Subject: [PATCH 26/58] ui: use existing style for empty job client status placeholder --- ui/app/styles/components.scss | 1 - ui/app/styles/components/job-client-status-row.scss | 9 --------- ui/app/templates/components/job-client-status-row.hbs | 2 +- 3 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 ui/app/styles/components/job-client-status-row.scss diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index 51f796b3a8b4..207f03bcca75 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -21,7 +21,6 @@ @import './components/gutter-toggle'; @import './components/image-file.scss'; @import './components/inline-definitions'; -@import './components/job-client-status-row'; @import './components/job-diff'; @import './components/json-viewer'; @import './components/legend'; diff --git a/ui/app/styles/components/job-client-status-row.scss b/ui/app/styles/components/job-client-status-row.scss deleted file mode 100644 index 6d93d3f66ab4..000000000000 --- a/ui/app/styles/components/job-client-status-row.scss +++ /dev/null @@ -1,9 +0,0 @@ -.job-client-status-row { - .allocation-summary { - .is-empty { - color: darken($grey-blue, 20%); - text-align: center; - font-style: italic; - } - } -} diff --git a/ui/app/templates/components/job-client-status-row.hbs b/ui/app/templates/components/job-client-status-row.hbs index 545620cd37e6..22945ca22305 100644 --- a/ui/app/templates/components/job-client-status-row.hbs +++ b/ui/app/templates/components/job-client-status-row.hbs @@ -33,7 +33,7 @@ {{else}} -
{{this.allocationSummaryPlaceholder}}
+
{{this.allocationSummaryPlaceholder}}
{{/if}} \ No newline at end of file From 00b53bb38609e61cd5dd9c7003615051d88664d6 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 21 Sep 2021 17:40:33 -0400 Subject: [PATCH 27/58] ui: revert automatic changes to distribution-bar.hbs --- .../templates/components/distribution-bar.hbs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/ui/app/templates/components/distribution-bar.hbs b/ui/app/templates/components/distribution-bar.hbs index 08c3d5f0fc70..9f42b9c7c70e 100644 --- a/ui/app/templates/components/distribution-bar.hbs +++ b/ui/app/templates/components/distribution-bar.hbs @@ -1,32 +1,28 @@ - + {{#if hasBlock}} - {{yield (hash data=this._data activeDatum=this.activeDatum)}} + {{yield (hash + data=this._data + activeDatum=this.activeDatum + )}} {{else}} -
+
    {{#each this._data as |datum index|}}
  1. - + {{datum.label}} - - {{datum.value}} - + {{datum.value}}
  2. {{/each}}
-{{/if}} \ No newline at end of file +{{/if}} From d0f9108fa1978ddb06e79fd007fe434750fb40d5 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 23 Sep 2021 18:16:01 -0400 Subject: [PATCH 28/58] ui: make job client status summary legend clickable --- ui/app/components/distribution-bar.js | 3 ++- ui/app/components/job-client-status-bar.js | 9 +++++++ ui/app/styles/charts/distribution-bar.scss | 14 ++++++++++ .../job-client-status-summary-legend-item.hbs | 3 +++ .../parts/job-client-status-summary.hbs | 26 +++++++------------ 5 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index 16e351eeaa5b..18a9efa3a3b3 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -34,11 +34,12 @@ export default class DistributionBar extends Component.extend(WindowResizable) { const data = copy(this.data, true); const sum = data.mapBy('value').reduce(sumAggregate, 0); - return data.map(({ label, value, className, layers }, index) => ({ + return data.map(({ label, value, className, layers, queryParams }, index) => ({ label, value, className, layers, + queryParams, index, percent: value / sum, offset: diff --git a/ui/app/components/job-client-status-bar.js b/ui/app/components/job-client-status-bar.js index 623eb82d519c..33a060ec8d4a 100644 --- a/ui/app/components/job-client-status-bar.js +++ b/ui/app/components/job-client-status-bar.js @@ -22,47 +22,56 @@ export default class JobClientStatusBar extends DistributionBar { lost, notScheduled, } = this.jobClientStatus.byStatus; + return [ { label: 'Queued', value: queued.length, className: 'queued', + queryParams: { status: JSON.stringify(['queued']) }, }, { label: 'Starting', value: starting.length, className: 'starting', + queryParams: { status: JSON.stringify(['starting']) }, layers: 2, }, { label: 'Running', value: running.length, className: 'running', + queryParams: { status: JSON.stringify(['running']) }, }, { label: 'Complete', value: complete.length, className: 'complete', + queryParams: { status: JSON.stringify(['complete']) }, }, { label: 'Degraded', value: degraded.length, className: 'degraded', + queryParams: { status: JSON.stringify(['degraded']) }, }, { label: 'Failed', value: failed.length, className: 'failed', + queryParams: { status: JSON.stringify(['failed']) }, }, { label: 'Lost', value: lost.length, className: 'lost', + queryParams: { status: JSON.stringify(['lost']) }, }, { label: 'Not Scheduled', value: notScheduled.length, className: 'not-scheduled', + queryParams: { status: JSON.stringify(['notScheduled']) }, }, ]; } diff --git a/ui/app/styles/charts/distribution-bar.scss b/ui/app/styles/charts/distribution-bar.scss index 14fa3716652c..edafacfdec8b 100644 --- a/ui/app/styles/charts/distribution-bar.scss +++ b/ui/app/styles/charts/distribution-bar.scss @@ -77,6 +77,20 @@ background-color: rgba($info, 0.1); } + &.is-clickable { + a { + display: block; + text-decoration: none; + color: inherit; + } + + &:not(.is-empty) { + &:hover { + background-color: rgba($info, 0.1); + } + } + } + &.is-empty { color: darken($grey-blue, 20%); border: none; diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs new file mode 100644 index 000000000000..d4eb4321fdfb --- /dev/null +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs @@ -0,0 +1,3 @@ + +{{@datum.value}} +{{@datum.label}} diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index eede8622911f..adf3d2f4ef8e 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -34,25 +34,17 @@ >
    {{#each chart.data as |datum index|}} -
  1. - - - {{datum.value}} - - - {{datum.label}} - +
  2. + {{#if (gt datum.value 0)}} + + + + {{else}} + + {{/if}}
  3. {{/each}}
- \ No newline at end of file + From eb1bcb038f8df9925d3fd00313b6465b13b24724 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 23 Sep 2021 18:29:17 -0400 Subject: [PATCH 29/58] ui: small fixes --- .../components/job-page/parts/summary.hbs | 1 + .../templates/components/job-page/system.hbs | 2 +- ui/app/templates/components/job-subnav.hbs | 4 +++- ui/app/templates/jobs/job/clients.hbs | 24 +++++-------------- ui/app/templates/jobs/job/index.hbs | 3 +-- 5 files changed, 12 insertions(+), 22 deletions(-) diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index 0fe77d45b94d..e90cdff53753 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -51,3 +51,4 @@ {{/component}} + diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 8734536b760d..1f1db67d1863 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -24,7 +24,7 @@ @jobClientStatus={{this.jobClientStatus}} @gotoClients={{this.gotoClients}} /> - + diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index fd4fc055cfad..62cecb3f58a8 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -8,6 +8,8 @@ {{/if}}
  • Allocations
  • Evaluations
  • -
  • Clients
  • + {{#if this.job.hasClientStatus}} +
  • Clients
  • + {{/if}}
    diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index dcb74e0ced7a..e4d9cad0325c 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -51,24 +51,12 @@ @class="with-foot" as |t| > - - Client ID - - - Client Name - - - Created - - - Modified - - - Job Status - - - Allocation Summary - + Client ID + Client Name + Created + Modified + Job Status + Allocation Summary Date: Thu, 23 Sep 2021 18:29:51 -0400 Subject: [PATCH 30/58] ui: add system and service jobs to sysbatchSmall mirage scenario --- ui/mirage/scenarios/sysbatch.js | 98 ++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/ui/mirage/scenarios/sysbatch.js b/ui/mirage/scenarios/sysbatch.js index 605861369512..86a840ace685 100644 --- a/ui/mirage/scenarios/sysbatch.js +++ b/ui/mirage/scenarios/sysbatch.js @@ -11,55 +11,65 @@ export function sysbatchSmall(server) { status: 'ready', }); - // Job with 1 task group. - const job1 = server.create('job', { + // Generate non-system/sysbatch job as counter-example. + server.create('job', { status: 'running', - datacenters: ['dc1', 'dc2'], - type: 'sysbatch', + type: 'service', resourceSpec: ['M: 256, C: 500'], - createAllocations: false, - }); - clients.forEach(c => { - server.create('allocation', { jobId: job1.id, nodeId: c.id }); + createAllocations: true, }); - // Job with 2 task groups. - const job2 = server.create('job', { - status: 'running', - datacenters: ['dc1'], - type: 'sysbatch', - resourceSpec: ['M: 256, C: 500', 'M: 256, C: 500'], - createAllocations: false, - }); - clients.forEach(c => { - server.create('allocation', { jobId: job2.id, nodeId: c.id }); - server.create('allocation', { jobId: job2.id, nodeId: c.id }); - }); + ['system', 'sysbatch'].forEach(type => { + // Job with 1 task group. + const job1 = server.create('job', { + status: 'running', + datacenters: ['dc1', 'dc2'], + type, + resourceSpec: ['M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach(c => { + server.create('allocation', { jobId: job1.id, nodeId: c.id }); + }); - // Job with 3 task groups. - const job3 = server.create('job', { - status: 'running', - datacenters: ['dc1'], - type: 'sysbatch', - resourceSpec: ['M: 256, C: 500', 'M: 256, C: 500', 'M: 256, C: 500'], - createAllocations: false, - }); - clients.forEach(c => { - server.create('allocation', { jobId: job3.id, nodeId: c.id }); - server.create('allocation', { jobId: job3.id, nodeId: c.id }); - server.create('allocation', { jobId: job3.id, nodeId: c.id }); - }); + // Job with 2 task groups. + const job2 = server.create('job', { + status: 'running', + datacenters: ['dc1'], + type, + resourceSpec: ['M: 256, C: 500', 'M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach(c => { + server.create('allocation', { jobId: job2.id, nodeId: c.id }); + server.create('allocation', { jobId: job2.id, nodeId: c.id }); + }); - // Job with client not scheduled. - const jobNotScheduled = server.create('job', { - status: 'running', - datacenters: ['dc1'], - type: 'sysbatch', - resourceSpec: ['M: 256, C: 500'], - createAllocations: false, - }); - clients.forEach((c, i) => { - if (i > clients.length - 3) return; - server.create('allocation', { jobId: jobNotScheduled.id, nodeId: c.id }); + // Job with 3 task groups. + const job3 = server.create('job', { + status: 'running', + datacenters: ['dc1'], + type, + resourceSpec: ['M: 256, C: 500', 'M: 256, C: 500', 'M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach(c => { + server.create('allocation', { jobId: job3.id, nodeId: c.id }); + server.create('allocation', { jobId: job3.id, nodeId: c.id }); + server.create('allocation', { jobId: job3.id, nodeId: c.id }); + }); + + // Job with client not scheduled. + const jobNotScheduled = server.create('job', { + status: 'running', + datacenters: ['dc1'], + type, + resourceSpec: ['M: 256, C: 500'], + createAllocations: false, + }); + clients.forEach((c, i) => { + if (i > clients.length - 3) return; + server.create('allocation', { jobId: jobNotScheduled.id, nodeId: c.id }); + }); }); } From a7e3568322dad17f9f49252397e20e0a21018dd7 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 24 Sep 2021 16:05:14 -0400 Subject: [PATCH 31/58] ui: lint --- ui/app/components/job-page/parts/job-client-status-summary.js | 1 - .../job-page/parts/job-client-status-summary-legend-item.hbs | 2 +- .../components/job-page/parts/job-client-status-summary.hbs | 4 ++-- ui/tests/unit/utils/job-client-status-test.js | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/app/components/job-page/parts/job-client-status-summary.js b/ui/app/components/job-page/parts/job-client-status-summary.js index 9faa03b5a150..209d5258b9ed 100644 --- a/ui/app/components/job-page/parts/job-client-status-summary.js +++ b/ui/app/components/job-page/parts/job-client-status-summary.js @@ -1,6 +1,5 @@ import Component from '@ember/component'; import { action, computed } from '@ember/object'; -import { inject as service } from '@ember/service'; import { classNames } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs index d4eb4321fdfb..a7c591b57fa7 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs @@ -1,3 +1,3 @@ - + {{@datum.value}} {{@datum.label}} diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index adf3d2f4ef8e..ead6b931b8bb 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -37,10 +37,10 @@
  • {{#if (gt datum.value 0)}} - + {{else}} - + {{/if}}
  • {{/each}} diff --git a/ui/tests/unit/utils/job-client-status-test.js b/ui/tests/unit/utils/job-client-status-test.js index 6afdd6f89edb..151dc85c4f0f 100644 --- a/ui/tests/unit/utils/job-client-status-test.js +++ b/ui/tests/unit/utils/job-client-status-test.js @@ -35,7 +35,7 @@ class NodeMock { } } -module('Unit | Util | JobClientStatus', function(hooks) { +module('Unit | Util | JobClientStatus', function() { test('it handles the case where all nodes are running', async function(assert) { const node = new NodeMock('node-1', 'dc1'); const nodes = [node]; From 41e35bd436e8b90e2f6bfc99fe9e29f74b10c90e Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 24 Sep 2021 19:30:55 -0400 Subject: [PATCH 32/58] ui: rename job details client page node class filter for client class --- ui/app/controllers/jobs/job/clients.js | 30 +++++++++++++------------- ui/app/templates/jobs/job/clients.hbs | 10 ++++----- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index d357dfe72bc2..1a76917c988e 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -13,10 +13,10 @@ import classic from 'ember-classic-decorator'; @classic export default class ClientsController extends Controller.extend( - SortableFactory(['id', 'name', 'jobStatus']), - Searchable, - WithNamespaceResetting - ) { + SortableFactory(['id', 'name', 'jobStatus']), + Searchable, + WithNamespaceResetting +) { queryParams = [ { currentPage: 'page', @@ -31,7 +31,7 @@ export default class ClientsController extends Controller.extend( qpDatacenter: 'dc', }, { - qpNodeClass: 'nodeclass', + qpClientClass: 'clientclass', }, { sortProperty: 'sort', @@ -43,7 +43,7 @@ export default class ClientsController extends Controller.extend( qpStatus = ''; qpDatacenter = ''; - qpNodeClass = ''; + qpClientClass = ''; currentPage = 1; pageSize = 25; @@ -53,7 +53,7 @@ export default class ClientsController extends Controller.extend( @selection('qpStatus') selectionStatus; @selection('qpDatacenter') selectionDatacenter; - @selection('qpNodeClass') selectionNodeClass; + @selection('qpClientClass') selectionClientClass; @alias('model') job; @jobClientStatus('allNodes', 'job') jobClientStatus; @@ -85,13 +85,13 @@ export default class ClientsController extends Controller.extend( 'jobClientStatus.byNode', 'selectionStatus', 'selectionDatacenter', - 'selectionNodeClass' + 'selectionClientClass' ) get filteredNodes() { const { selectionStatus: statuses, selectionDatacenter: datacenters, - selectionNodeClass: nodeclasses, + selectionClientClass: clientClasses, } = this; return this.nodes @@ -102,7 +102,7 @@ export default class ClientsController extends Controller.extend( if (datacenters.length && !datacenters.includes(node.datacenter)) { return false; } - if (nodeclasses.length && !nodeclasses.includes(node.nodeClass)) { + if (clientClasses.length && !clientClasses.includes(node.nodeClass)) { return false; } @@ -148,17 +148,17 @@ export default class ClientsController extends Controller.extend( return datacenters.sort().map(dc => ({ key: dc, label: dc })); } - @computed('selectionNodeClass', 'nodes') - get optionsNodeClass() { - const nodeClasses = Array.from(new Set(this.nodes.mapBy('nodeClass'))).compact(); + @computed('selectionClientClass', 'nodes') + get optionsClientClass() { + const clientClasses = Array.from(new Set(this.nodes.mapBy('nodeClass'))).compact(); // Update query param when the list of datacenters changes. scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects - this.set('qpNodeClass', serialize(intersection(nodeClasses, this.selectionNodeClass))); + this.set('qpClientClass', serialize(intersection(clientClasses, this.selectionClientClass))); }); - return nodeClasses.sort().map(nodeClass => ({ key: nodeClass, label: nodeClass })); + return clientClasses.sort().map(clientClass => ({ key: clientClass, label: clientClass })); } @action diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index e4d9cad0325c..225fc0645d52 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -28,11 +28,11 @@ @onSelect={{action this.setFacetQueryParam "qpDatacenter"}} /> From 081224ef1a1fd8678cb388f4161c684377747ed0 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 24 Sep 2021 19:31:52 -0400 Subject: [PATCH 33/58] ui: use links for short id in the job details client tab --- ui/app/templates/components/job-client-status-row.hbs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/templates/components/job-client-status-row.hbs b/ui/app/templates/components/job-client-status-row.hbs index 22945ca22305..5d34073920a5 100644 --- a/ui/app/templates/components/job-client-status-row.hbs +++ b/ui/app/templates/components/job-client-status-row.hbs @@ -1,6 +1,8 @@ - {{this.row.node.shortId}} + + {{this.row.node.shortId}} + {{this.row.node.name}} From 6473b9785762c8be530176bb6910de9641ceaed9 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 24 Sep 2021 19:32:32 -0400 Subject: [PATCH 34/58] ui: fix sorting by id and name in the job details client tab --- ui/app/templates/jobs/job/clients.hbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index 225fc0645d52..c54209cba799 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -51,8 +51,8 @@ @class="with-foot" as |t| > - Client ID - Client Name + Client ID + Client Name Created Modified Job Status From 4ac392d9af123a8cb3b21a80c708d98dbddacb24 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 27 Sep 2021 17:18:09 -0400 Subject: [PATCH 35/58] tmp --- .../components/job-client-status-row.hbs | 8 +- ui/app/templates/jobs/job/clients.hbs | 3 +- ui/tests/acceptance/job-clients-test.js | 191 ++++++++++++++++-- ui/tests/pages/components/clients.js | 27 ++- ui/tests/pages/jobs/job/clients.js | 7 + 5 files changed, 200 insertions(+), 36 deletions(-) diff --git a/ui/app/templates/components/job-client-status-row.hbs b/ui/app/templates/components/job-client-status-row.hbs index 5d34073920a5..e815ccf4b087 100644 --- a/ui/app/templates/components/job-client-status-row.hbs +++ b/ui/app/templates/components/job-client-status-row.hbs @@ -1,10 +1,10 @@ - + {{this.row.node.shortId}} - + {{this.row.node.name}} @@ -25,11 +25,11 @@ - {{/if}} - + {{this.humanizedJobStatus}} - + {{#if this.shouldDisplayAllocationSummary}}
    diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index c54209cba799..32ae624312e4 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -14,7 +14,7 @@
    diff --git a/ui/tests/acceptance/job-clients-test.js b/ui/tests/acceptance/job-clients-test.js index e613f788d9ac..089104ea859a 100644 --- a/ui/tests/acceptance/job-clients-test.js +++ b/ui/tests/acceptance/job-clients-test.js @@ -8,14 +8,16 @@ import Clients from 'nomad-ui/tests/pages/jobs/job/clients'; let job; let clients; -const makeSearchableClients = server => { +const makeSearchableClients = (server, job) => { Array(10) .fill(null) .map((_, index) => { - server.create('node', { + const node = server.create('node', { id: index < 5 ? `ffffff-dddddd-${index}` : `111111-222222-${index}`, - shallow: true, + datacenter: 'dc1', + status: 'ready', }); + server.create('allocation', { jobId: job.id, nodeId: node.id }); }); }; @@ -39,6 +41,15 @@ module('Acceptance | job clients', function(hooks) { clients.forEach(c => { server.create('allocation', { jobId: job.id, nodeId: c.id }); }); + + // Create clients without allocations for test job so the jos status is + // 'not scheduled'. + clients = clients.concat( + server.createList('node', 3, { + datacenter: 'dc1', + status: 'ready', + }) + ); }); test('it passes an accessibility audit', async function(assert) { @@ -48,43 +59,58 @@ module('Acceptance | job clients', function(hooks) { test('lists all clients for the job', async function(assert) { await Clients.visit({ id: job.id }); - assert.equal(Clients.clients.length, 12, 'Clients are shown in a table'); - - const sortedClients = clients; + assert.equal(Clients.clients.length, 15, 'Clients are shown in a table'); - Clients.clients.forEach((client, index) => { - const shortId = sortedClients[index].id.split('-')[0]; - console.log('client\n\n', client); - assert.equal(client.shortId, shortId, `Client ${index} is ${shortId}`); - }); + const clientIDs = clients.sortBy('id').map(c => c.id); + const clientsInTable = Clients.clients.map(c => c.id).sort(); + assert.deepEqual(clientsInTable, clientIDs); assert.equal(document.title, `Job ${job.name} clients - Nomad`); }); + test('dates have tooltip', async function(assert) { + await Clients.visit({ id: job.id }); + + Clients.clients.forEach((clientRow, index) => { + const jobStatus = Clients.clientFor(clientRow.id).status; + + ['createTime', 'modifyTime'].forEach(col => { + if (jobStatus === 'not scheduled') { + assert.equal(clientRow[col].text, '-', `row ${index} doesn't have ${col} tooltip`); + return; + } + + const hasTooltip = clientRow[col].tooltip.isPresent; + const tooltipText = clientRow[col].tooltip.text; + assert.true(hasTooltip, `row ${index} has ${col} tooltip`); + assert.ok(tooltipText, `row ${index} has ${col} tooltip content ${tooltipText}`); + }); + }); + }); + test('clients table is sortable', async function(assert) { await Clients.visit({ id: job.id }); - await Clients.sortBy('modifyTime'); + await Clients.sortBy('node.name'); assert.equal( currentURL(), - `/jobs/${job.id}/clients?desc=true&sort=modifyTime`, + `/jobs/${job.id}/clients?desc=true&sort=node.name`, 'the URL persists the sort parameter' ); - const sortedClients = clients.sortBy('modifyTime').reverse(); + + const sortedClients = clients.sortBy('name').reverse(); Clients.clients.forEach((client, index) => { const shortId = sortedClients[index].id.split('-')[0]; assert.equal( client.shortId, shortId, - `Client ${index} is ${shortId} with modify time ${sortedClients[index].modifyTime}` + `Client ${index} is ${shortId} with name ${sortedClients[index].name}` ); }); }); test('clients table is searchable', async function(assert) { - makeSearchableClients(server); - - clients = server.schema.nodes.where({ jobId: job.id }).models; + makeSearchableClients(server, job); await Clients.visit({ id: job.id }); await Clients.search('ffffff'); @@ -93,9 +119,7 @@ module('Acceptance | job clients', function(hooks) { }); test('when a search yields no results, the search box remains', async function(assert) { - makeSearchableClients(server); - - clients = server.schema.nodes.where({ jobId: job.id }).models; + makeSearchableClients(server, job); await Clients.visit({ id: job.id }); await Clients.search('^nothing will ever match this long regex$'); @@ -123,4 +147,129 @@ module('Acceptance | job clients', function(hooks) { assert.ok(Clients.error.isPresent, 'Error message is shown'); assert.equal(Clients.error.title, 'Not Found', 'Error message is for 404'); }); + + test('clicking row goes to client details', async function(assert) { + const client = clients[0]; + + await Clients.visit({ id: job.id }); + await Clients.clientFor(client.id).click(); + assert.equal(currentURL(), `/clients/${client.id}`); + + await Clients.visit({ id: job.id }); + await Clients.clientFor(client.id).visit(); + assert.equal(currentURL(), `/clients/${client.id}`); + + await Clients.visit({ id: job.id }); + await Clients.clientFor(client.id).visitRow(); + assert.equal(currentURL(), `/clients/${client.id}`); + }); + + testFacet('Job Status', { + facet: Clients.facets.jobStatus, + paramName: 'jobStatus', + expectedOptions: [ + 'Queued', + 'Not Scheduled', + 'Starting', + 'Running', + 'Complete', + 'Degraded', + 'Failed', + 'Lost', + ], + async beforeEach() { + await Clients.visit({ id: job.id }); + }, + }); + + function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) { + test(`the ${label} facet has the correct options`, async function(assert) { + await beforeEach(); + await facet.toggle(); + + let expectation; + if (typeof expectedOptions === 'function') { + expectation = expectedOptions(); + } else { + expectation = expectedOptions; + } + + assert.deepEqual( + facet.options.map(option => option.label.trim()), + expectation, + `Options for facet ${paramName} are as expected` + ); + }); + + // test(`the ${label} facet filters the nodes list by ${label}`, async function(assert) { + // let option; + + // await beforeEach(); + + // await facet.toggle(); + // option = facet.options.objectAt(0); + // await option.toggle(); + + // const selection = [option.key]; + // const expectedNodes = server.db.nodes + // .filter(node => filter(node, selection)) + // .sortBy('modifyIndex') + // .reverse(); + + // ClientsList.nodes.forEach((node, index) => { + // assert.equal( + // node.id, + // expectedNodes[index].id.split('-')[0], + // `Node at ${index} is ${expectedNodes[index].id}` + // ); + // }); + // }); + + // test(`selecting multiple options in the ${label} facet results in a broader search`, async function(assert) { + // const selection = []; + + // await beforeEach(); + // await facet.toggle(); + + // const option1 = facet.options.objectAt(0); + // const option2 = facet.options.objectAt(1); + // await option1.toggle(); + // selection.push(option1.key); + // await option2.toggle(); + // selection.push(option2.key); + + // const expectedNodes = server.db.nodes + // .filter(node => filter(node, selection)) + // .sortBy('modifyIndex') + // .reverse(); + + // ClientsList.nodes.forEach((node, index) => { + // assert.equal( + // node.id, + // expectedNodes[index].id.split('-')[0], + // `Node at ${index} is ${expectedNodes[index].id}` + // ); + // }); + // }); + + // test(`selecting options in the ${label} facet updates the ${paramName} query param`, async function(assert) { + // const selection = []; + + // await beforeEach(); + // await facet.toggle(); + + // const option1 = facet.options.objectAt(0); + // const option2 = facet.options.objectAt(1); + // await option1.toggle(); + // selection.push(option1.key); + // await option2.toggle(); + // selection.push(option2.key); + + // assert.equal( + // currentURL(), + // `/clients?${paramName}=${encodeURIComponent(JSON.stringify(selection))}`, + // 'URL has the correct query param key and value' + // ); + // }); + } }); diff --git a/ui/tests/pages/components/clients.js b/ui/tests/pages/components/clients.js index 649802977832..7179c48c048e 100644 --- a/ui/tests/pages/components/clients.js +++ b/ui/tests/pages/components/clients.js @@ -1,25 +1,34 @@ -import { attribute, collection, clickable, text } from 'ember-cli-page-object'; +import { attribute, collection, clickable, hasClass, text } from 'ember-cli-page-object'; import { singularize } from 'ember-inflector'; export default function(selector = '[data-test-client]', propKey = 'clients') { const lookupKey = `${singularize(propKey)}For`; // Remove the bracket notation const attr = selector.substring(1, selector.length - 1); + return { [propKey]: collection(selector, { id: attribute(attr), shortId: text('[data-test-short-id]'), - createTime: text('[data-test-create-time]'), - createTooltip: attribute('aria-label', '[data-test-create-time] .tooltip'), - modifyTime: text('[data-test-modify-time]'), - status: text('[data-test-client-status]'), - job: text('[data-test-job]'), - client: text('[data-test-client]'), + name: text('[data-test-name]'), + createTime: { + scope: '[data-test-create-time]', + tooltip: { + scope: '.tooltip', + text: attribute('aria-label'), + }, + }, + modifyTime: { + scope: '[data-test-modify-time]', + tooltip: { + scope: '.tooltip', + text: attribute('aria-label'), + }, + }, + status: text('[data-test-job-status]'), visit: clickable('[data-test-short-id] a'), visitRow: clickable(), - visitJob: clickable('[data-test-job]'), - visitClient: clickable('[data-test-client] a'), }), [lookupKey]: function(id) { diff --git a/ui/tests/pages/jobs/job/clients.js b/ui/tests/pages/jobs/job/clients.js index 8df5401c5b5a..83d69fa9e511 100644 --- a/ui/tests/pages/jobs/job/clients.js +++ b/ui/tests/pages/jobs/job/clients.js @@ -8,6 +8,7 @@ import { text, visitable, } from 'ember-cli-page-object'; +import { multiFacet } from 'nomad-ui/tests/pages/components/facet'; import clients from 'nomad-ui/tests/pages/components/clients'; import error from 'nomad-ui/tests/pages/components/error'; @@ -38,5 +39,11 @@ export default create({ .sort(); }, + facets: { + jobStatus: multiFacet('[data-test-job-status-facet]'), + datacenter: multiFacet('[data-test-datacenter-facet]'), + clientClass: multiFacet('[data-test-class-facet]'), + }, + error: error(), }); From e33150347b0729e9395222a93d014b47d7ddf2ba Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 28 Sep 2021 17:09:29 -0400 Subject: [PATCH 36/58] ui: fix pagination on job details client tab --- ui/app/templates/jobs/job/clients.hbs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index 32ae624312e4..8efb22eb3cc0 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -68,16 +68,10 @@
    From 619938f82905bba107c65e1e82de111fb2a3a82b Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 28 Sep 2021 17:53:45 -0400 Subject: [PATCH 37/58] ui: add TODO for job status client facet tests --- ui/tests/acceptance/job-clients-test.js | 76 +------------------------ 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/ui/tests/acceptance/job-clients-test.js b/ui/tests/acceptance/job-clients-test.js index 089104ea859a..34583f1d90bd 100644 --- a/ui/tests/acceptance/job-clients-test.js +++ b/ui/tests/acceptance/job-clients-test.js @@ -42,8 +42,7 @@ module('Acceptance | job clients', function(hooks) { server.create('allocation', { jobId: job.id, nodeId: c.id }); }); - // Create clients without allocations for test job so the jos status is - // 'not scheduled'. + // Create clients without allocations to have some 'not scheduled' job status. clients = clients.concat( server.createList('node', 3, { datacenter: 'dc1', @@ -182,7 +181,7 @@ module('Acceptance | job clients', function(hooks) { }, }); - function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) { + function testFacet(label, { facet, paramName, beforeEach, expectedOptions }) { test(`the ${label} facet has the correct options`, async function(assert) { await beforeEach(); await facet.toggle(); @@ -201,75 +200,6 @@ module('Acceptance | job clients', function(hooks) { ); }); - // test(`the ${label} facet filters the nodes list by ${label}`, async function(assert) { - // let option; - - // await beforeEach(); - - // await facet.toggle(); - // option = facet.options.objectAt(0); - // await option.toggle(); - - // const selection = [option.key]; - // const expectedNodes = server.db.nodes - // .filter(node => filter(node, selection)) - // .sortBy('modifyIndex') - // .reverse(); - - // ClientsList.nodes.forEach((node, index) => { - // assert.equal( - // node.id, - // expectedNodes[index].id.split('-')[0], - // `Node at ${index} is ${expectedNodes[index].id}` - // ); - // }); - // }); - - // test(`selecting multiple options in the ${label} facet results in a broader search`, async function(assert) { - // const selection = []; - - // await beforeEach(); - // await facet.toggle(); - - // const option1 = facet.options.objectAt(0); - // const option2 = facet.options.objectAt(1); - // await option1.toggle(); - // selection.push(option1.key); - // await option2.toggle(); - // selection.push(option2.key); - - // const expectedNodes = server.db.nodes - // .filter(node => filter(node, selection)) - // .sortBy('modifyIndex') - // .reverse(); - - // ClientsList.nodes.forEach((node, index) => { - // assert.equal( - // node.id, - // expectedNodes[index].id.split('-')[0], - // `Node at ${index} is ${expectedNodes[index].id}` - // ); - // }); - // }); - - // test(`selecting options in the ${label} facet updates the ${paramName} query param`, async function(assert) { - // const selection = []; - - // await beforeEach(); - // await facet.toggle(); - - // const option1 = facet.options.objectAt(0); - // const option2 = facet.options.objectAt(1); - // await option1.toggle(); - // selection.push(option1.key); - // await option2.toggle(); - // selection.push(option2.key); - - // assert.equal( - // currentURL(), - // `/clients?${paramName}=${encodeURIComponent(JSON.stringify(selection))}`, - // 'URL has the correct query param key and value' - // ); - // }); + // TODO: add facet tests for actual list filtering } }); From 5577d0d00c24ff3b8486561dcf10f4d0dd1f3f5d Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Wed, 29 Sep 2021 18:56:46 -0400 Subject: [PATCH 38/58] ui: fix tests for job client status in job details page --- ui/app/components/distribution-bar.js | 2 +- ui/app/components/job-client-status-bar.js | 2 +- .../parts/job-client-status-summary.hbs | 2 +- ui/app/utils/properties/job-client-status.js | 9 +- ui/mirage/scenarios/sysbatch.js | 10 +- ui/tests/acceptance/job-detail-test.js | 24 +++- ui/tests/helpers/module-for-job.js | 113 +++++++++++------- .../components/client-status-bar-test.js | 71 ----------- .../components/job-client-status-bar-test.js | 72 +++++++++++ .../pages/components/client-status-bar.js | 15 --- ui/tests/pages/components/clients.js | 4 +- .../pages/components/job-client-status-bar.js | 37 ++++++ ui/tests/pages/jobs/detail.js | 12 +- 13 files changed, 221 insertions(+), 152 deletions(-) delete mode 100644 ui/tests/integration/components/client-status-bar-test.js create mode 100644 ui/tests/integration/components/job-client-status-bar-test.js delete mode 100644 ui/tests/pages/components/client-status-bar.js create mode 100644 ui/tests/pages/components/job-client-status-bar.js diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index 18a9efa3a3b3..6e9fd8c4dbaa 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -134,7 +134,7 @@ export default class DistributionBar extends Component.extend(WindowResizable) { isInactive && 'inactive', isClickable && 'clickable' ].compact().join(' '); - }); + }).attr('data-test-slice-label', d => d.className); this.set('slices', slices); diff --git a/ui/app/components/job-client-status-bar.js b/ui/app/components/job-client-status-bar.js index 33a060ec8d4a..52d4c44f2cc8 100644 --- a/ui/app/components/job-client-status-bar.js +++ b/ui/app/components/job-client-status-bar.js @@ -6,7 +6,7 @@ import classic from 'ember-classic-decorator'; export default class JobClientStatusBar extends DistributionBar { layoutName = 'components/distribution-bar'; - 'data-test-client-status-bar' = true; + 'data-test-job-client-status-bar' = true; jobClientStatus = null; onSliceClick() {} diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index ead6b931b8bb..b258685eea08 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -34,7 +34,7 @@ >
      {{#each chart.data as |datum index|}} -
    1. +
    2. {{#if (gt datum.value 0)}} diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index 4ec57ecbf3fe..1988d33d7567 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -18,14 +18,15 @@ const STATUS = [ export default function jobClientStatus(nodesKey, jobKey) { return computed(nodesKey, `${jobKey}.{datacenters,status,allocations,taskGroups}`, function() { const job = this.get(jobKey); + const nodes = this.get(nodesKey); // Filter nodes by the datacenters defined in the job. - const nodes = this.get(nodesKey).filter(n => { + const filteredNodes = nodes.filter(n => { return job.datacenters.indexOf(n.datacenter) >= 0; }); if (job.status === 'pending') { - return allQueued(nodes); + return allQueued(filteredNodes); } // Group the job allocations by the ID of the client that is running them. @@ -41,9 +42,9 @@ export default function jobClientStatus(nodesKey, jobKey) { const result = { byNode: {}, byStatus: {}, - totalNodes: nodes.length, + totalNodes: filteredNodes.length, }; - nodes.forEach(n => { + filteredNodes.forEach(n => { const status = jobStatus(allocsByNodeID[n.id], job.taskGroups.length); result.byNode[n.id] = status; diff --git a/ui/mirage/scenarios/sysbatch.js b/ui/mirage/scenarios/sysbatch.js index 86a840ace685..5bb4b57c7572 100644 --- a/ui/mirage/scenarios/sysbatch.js +++ b/ui/mirage/scenarios/sysbatch.js @@ -1,6 +1,14 @@ export function sysbatchSmall(server) { + return sysbatchScenario(server, 15); +} + +export function sysbatchLarge(server) { + return sysbatchScenario(server, 55); +} + +function sysbatchScenario(server, clientCount) { server.createList('agent', 3); - const clients = server.createList('node', 12, { + const clients = server.createList('node', clientCount, { datacenter: 'dc1', status: 'ready', }); diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index 38ce795c2b3d..3b5c737c4910 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -5,19 +5,39 @@ import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import moment from 'moment'; import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; -import moduleForJob from 'nomad-ui/tests/helpers/module-for-job'; +import moduleForJob, { moduleForJobWithClientStatus } from 'nomad-ui/tests/helpers/module-for-job'; import JobDetail from 'nomad-ui/tests/pages/jobs/detail'; moduleForJob('Acceptance | job detail (batch)', 'allocations', () => server.create('job', { type: 'batch', shallow: true }) ); + moduleForJob('Acceptance | job detail (system)', 'allocations', () => server.create('job', { type: 'system', shallow: true }) ); -moduleForJob('Acceptance | job detail (sysbatch)', 'sysbatch', () => + +moduleForJobWithClientStatus('Acceptance | job detail with client status (system)', () => + server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'system', + createAllocations: false, + }) +); + +moduleForJob('Acceptance | job detail (sysbatch)', 'allocations', () => server.create('job', { type: 'sysbatch', shallow: true }) ); +moduleForJobWithClientStatus('Acceptance | job detail with client status (sysbatch)', () => + server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'sysbatch', + createAllocations: false, + }) +); + moduleForJob( 'Acceptance | job detail (periodic)', 'children', diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index ba68378e228e..5e58ad162503 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -1,4 +1,4 @@ -import { click, currentURL } from '@ember/test-helpers'; +import { currentURL } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -12,36 +12,17 @@ export default function moduleForJob(title, context, jobFactory, additionalTests setupApplicationTest(hooks); setupMirage(hooks); hooks.before(function() { - if (context !== 'allocations' && context !== 'children' && context !== 'sysbatch') { + if (context !== 'allocations' && context !== 'children') { throw new Error( - `Invalid context provided to moduleForJob, expected either "allocations", "sysbatch" or "children", got ${context}` + `Invalid context provided to moduleForJob, expected either "allocations" or "children", got ${context}` ); } }); hooks.beforeEach(async function() { - if (context === 'sysbatch') { - const clients = server.createList('node', 12, { - datacenter: 'dc1', - status: 'ready', - }); - // Job with 1 task group. - job = server.create('job', { - status: 'running', - datacenters: ['dc1', 'dc2'], - type: 'sysbatch', - resourceSpec: ['M: 256, C: 500'], - createAllocations: false, - }); - clients.forEach(c => { - server.create('allocation', { jobId: job.id, nodeId: c.id }); - }); - await JobDetail.visit({ id: job.id }); - } else { - server.create('node'); - job = jobFactory(); - await JobDetail.visit({ id: job.id }); - } + server.create('node'); + job = jobFactory(); + await JobDetail.visit({ id: job.id }); }); test('visiting /jobs/:job_id', async function(assert) { @@ -81,26 +62,6 @@ export default function moduleForJob(title, context, jobFactory, additionalTests } }); - if (context === 'sysbatch') { - test('clients for the job are showing in the overview', async function(assert) { - assert.ok( - JobDetail.clientSummary.isPresent, - 'Client Summary Status Bar Chart is displayed in summary section' - ); - }); - test('clicking a status bar in the chart takes you to a pre-filtered view of clients', async function(assert) { - const bars = document.querySelectorAll('[data-test-client-status-bar] > svg > g > g'); - const status = bars[0].className.baseVal; - await click(`[data-test-client-status-${status}="${status}"]`); - const encodedStatus = statusList => encodeURIComponent(JSON.stringify(statusList)); - assert.equal( - currentURL(), - `/jobs/${job.name}/clients?status=${encodedStatus([status])}`, - 'Client Status Bar Chart links to client tab' - ); - }); - } - if (context === 'allocations') { test('allocations for the job are shown in the overview', async function(assert) { assert.ok(JobDetail.allocationsSummary, 'Allocations are shown in the summary section'); @@ -138,3 +99,65 @@ export default function moduleForJob(title, context, jobFactory, additionalTests } }); } + +// eslint-disable-next-line ember/no-test-module-for +export function moduleForJobWithClientStatus(title, jobFactory, additionalTests) { + let job; + + module(title, function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function() { + const clients = server.createList('node', 3, { + datacenter: 'dc1', + status: 'ready', + }); + job = jobFactory(); + clients.forEach(c => { + server.create('allocation', { jobId: job.id, nodeId: c.id }); + }); + await JobDetail.visit({ id: job.id }); + }); + + test('job status summary is shown in the overview', async function(assert) { + assert.ok( + JobDetail.jobClientStatusSummary.isPresent, + 'Summary bar is displayed in the Job Status in Client summary section' + ); + }); + + test('clicking legend item navigates to a pre-filtered clients table', async function(assert) { + const legendItem = JobDetail.jobClientStatusSummary.legend.clickableItems[0]; + const encodedStatus = encodeURIComponent(JSON.stringify([legendItem.label])); + await legendItem.click(); + + // await this.pauseTest(); + + assert.equal( + currentURL(), + `/jobs/${job.name}/clients?status=${encodedStatus}`, + 'Client Status Bar Chart legend links to client tab' + ); + }); + + // TODO: fix click event on slices during tests + // test('clicking in a slice takes you to a pre-filtered view of clients', async function(assert) { + // const slice = JobDetail.jobClientStatusSummary.slices[0]; + // await slice.click(); + // + // const encodedStatus = encodeURIComponent(JSON.stringify([slice.label])); + // assert.equal( + // currentURL(), + // `/jobs/${job.name}/clients?status=${encodedStatus}`, + // 'Client Status Bar Chart links to client tab' + // ); + // }); + + for (var testName in additionalTests) { + test(testName, async function(assert) { + await additionalTests[testName].call(this, job, assert); + }); + } + }); +} diff --git a/ui/tests/integration/components/client-status-bar-test.js b/ui/tests/integration/components/client-status-bar-test.js deleted file mode 100644 index 948a3ff07ed1..000000000000 --- a/ui/tests/integration/components/client-status-bar-test.js +++ /dev/null @@ -1,71 +0,0 @@ -import { module, test } from 'qunit'; -import { create } from 'ember-cli-page-object'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import sinon from 'sinon'; -import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; -import clientStatusBar from 'nomad-ui/tests/pages/components/client-status-bar'; - -const ClientStatusBar = create(clientStatusBar()); - -module('Integration | Component | client-status-bar', function(hooks) { - setupRenderingTest(hooks); - - const commonProperties = () => ({ - onBarClick: sinon.spy(), - jobClientStatus: { - byStatus: { - queued: [], - starting: ['someNodeId'], - running: [], - complete: [], - degraded: [], - failed: [], - lost: [], - notScheduled: [], - }, - }, - isNarrow: true, - }); - - const commonTemplate = hbs` - `; - - test('it renders', async function(assert) { - const props = commonProperties(); - this.setProperties(props); - await render(commonTemplate); - - assert.ok(ClientStatusBar.isPresent, 'Client Status Bar is rendered'); - await componentA11yAudit(this.element, assert); - }); - - test('it fires the onBarClick handler method when clicking a bar in the chart', async function(assert) { - const props = commonProperties(); - this.setProperties(props); - await render(commonTemplate); - await ClientStatusBar.visitBar('starting'); - assert.ok(props.onBarClick.calledOnce); - }); - - test('it handles an update to client status property', async function(assert) { - const props = commonProperties(); - this.setProperties(props); - await render(commonTemplate); - const newProps = { - ...props, - jobClientStatus: { - ...props.jobClientStatus, - byStatus: { ...props.jobClientStatus.byStatus, starting: [], running: ['someNodeId'] }, - }, - }; - this.setProperties(newProps); - await ClientStatusBar.visitBar('running'); - assert.ok(props.onBarClick.calledOnce); - }); -}); diff --git a/ui/tests/integration/components/job-client-status-bar-test.js b/ui/tests/integration/components/job-client-status-bar-test.js new file mode 100644 index 000000000000..3abe86248e38 --- /dev/null +++ b/ui/tests/integration/components/job-client-status-bar-test.js @@ -0,0 +1,72 @@ +import { module, test } from 'qunit'; +import { create } from 'ember-cli-page-object'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; +import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; +import jobClientStatusBar from 'nomad-ui/tests/pages/components/job-client-status-bar'; + +const JobClientStatusBar = create(jobClientStatusBar()); + +module('Integration | Component | job-client-status-bar', function(hooks) { + setupRenderingTest(hooks); + + const commonProperties = () => ({ + onBarClick: sinon.spy(), + jobClientStatus: { + byStatus: { + queued: [], + starting: ['someNodeId'], + running: [], + complete: [], + degraded: [], + failed: [], + lost: [], + notScheduled: [], + }, + }, + isNarrow: true, + }); + + const commonTemplate = hbs` + `; + + test('it renders', async function(assert) { + const props = commonProperties(); + this.setProperties(props); + await render(commonTemplate); + + assert.ok(JobClientStatusBar.isPresent, 'Client Status Bar is rendered'); + await componentA11yAudit(this.element, assert); + }); + + // TODO: fix tests for slice click + // test('it fires the onBarClick handler method when clicking a bar in the chart', async function(assert) { + // const props = commonProperties(); + // this.setProperties(props); + // await render(commonTemplate); + // await JobClientStatusBar.slices[0].click(); + // assert.ok(props.onBarClick.calledOnce); + // }); + // + // test('it handles an update to client status property', async function(assert) { + // const props = commonProperties(); + // this.setProperties(props); + // await render(commonTemplate); + // const newProps = { + // ...props, + // jobClientStatus: { + // ...props.jobClientStatus, + // byStatus: { ...props.jobClientStatus.byStatus, starting: [], running: ['someNodeId'] }, + // }, + // }; + // this.setProperties(newProps); + // await JobClientStatusBar.visitSlice('running'); + // assert.ok(props.onBarClick.calledOnce); + // }); +}); diff --git a/ui/tests/pages/components/client-status-bar.js b/ui/tests/pages/components/client-status-bar.js deleted file mode 100644 index 46e95de6d1f9..000000000000 --- a/ui/tests/pages/components/client-status-bar.js +++ /dev/null @@ -1,15 +0,0 @@ -import { attribute, clickable, collection } from 'ember-cli-page-object'; - -export default scope => ({ - scope, - - bars: collection('.bars > g', { - id: attribute('data-test-client-status'), - visit: clickable(), - }), - - visitBar: async function(id) { - const bar = this.bars.toArray().findBy('id', id); - await bar.visit(); - }, -}); diff --git a/ui/tests/pages/components/clients.js b/ui/tests/pages/components/clients.js index 7179c48c048e..3a6e31e098ae 100644 --- a/ui/tests/pages/components/clients.js +++ b/ui/tests/pages/components/clients.js @@ -11,6 +11,8 @@ export default function(selector = '[data-test-client]', propKey = 'clients') { id: attribute(attr), shortId: text('[data-test-short-id]'), name: text('[data-test-name]'), + status: text('[data-test-job-status]'), + createTime: { scope: '[data-test-create-time]', tooltip: { @@ -18,6 +20,7 @@ export default function(selector = '[data-test-client]', propKey = 'clients') { text: attribute('aria-label'), }, }, + modifyTime: { scope: '[data-test-modify-time]', tooltip: { @@ -25,7 +28,6 @@ export default function(selector = '[data-test-client]', propKey = 'clients') { text: attribute('aria-label'), }, }, - status: text('[data-test-job-status]'), visit: clickable('[data-test-short-id] a'), visitRow: clickable(), diff --git a/ui/tests/pages/components/job-client-status-bar.js b/ui/tests/pages/components/job-client-status-bar.js new file mode 100644 index 000000000000..3cd753159bea --- /dev/null +++ b/ui/tests/pages/components/job-client-status-bar.js @@ -0,0 +1,37 @@ +import { attribute, clickable, collection } from 'ember-cli-page-object'; + +export default scope => ({ + scope, + + slices: collection('svg .bars g', { + label: attribute('data-test-slice-label'), + click: clickable(), + }), + + legend: { + scope: '.legend', + + items: collection('li', { + label: attribute('data-test-legent-label'), + }), + + clickableItems: collection('li.is-clickable', { + label: attribute('data-test-legent-label'), + click: clickable('a'), + }), + }, + + visitSlice: async function(label) { + await this.slices + .toArray() + .findBy('label', label) + .click(); + }, + + visitLegend: async function(label) { + await this.legend.clickableItems + .toArray() + .findBy('label', label) + .click(); + }, +}); diff --git a/ui/tests/pages/jobs/detail.js b/ui/tests/pages/jobs/detail.js index 3d86d422e7e4..c322eb7c7ea4 100644 --- a/ui/tests/pages/jobs/detail.js +++ b/ui/tests/pages/jobs/detail.js @@ -13,6 +13,7 @@ import { import allocations from 'nomad-ui/tests/pages/components/allocations'; import twoStepButton from 'nomad-ui/tests/pages/components/two-step-button'; import recommendationAccordion from 'nomad-ui/tests/pages/components/recommendation-accordion'; +import jobClientStatusBar from 'nomad-ui/tests/pages/components/job-client-status-bar'; export default create({ visit: visitable('/jobs/:id'), @@ -48,8 +49,6 @@ export default create({ isDisabled: property('disabled'), }, - barChart: isPresent('data-test-client-status-bar'), - stats: collection('[data-test-job-stat]', { id: attribute('data-test-job-stat'), text: text(), @@ -59,6 +58,7 @@ export default create({ return this.stats.toArray().findBy('id', id); }, + jobClientStatusSummary: jobClientStatusBar('[data-test-job-client-status-bar]'), childrenSummary: isPresent('[data-test-job-summary] [data-test-children-status-bar]'), allocationsSummary: isPresent('[data-test-job-summary] [data-test-allocation-status-bar]'), @@ -90,12 +90,4 @@ export default create({ recentAllocationsEmptyState: { headline: text('[data-test-empty-recent-allocations-headline]'), }, - - clientSummary: { - id: attribute('[data-test-client-status-bar]'), - }, - visitClients: function(attr) { - console.log('runs\n\n', attr); - clickable(attr); - }, }); From c36396bd9cc227e03d02fc9cff78c5a77507bf75 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Wed, 29 Sep 2021 19:20:02 -0400 Subject: [PATCH 39/58] ui: fix linting --- ui/app/controllers/jobs/job/clients.js | 8 ++++---- ui/tests/pages/components/clients.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index 1a76917c988e..2e14798bf566 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -13,10 +13,10 @@ import classic from 'ember-classic-decorator'; @classic export default class ClientsController extends Controller.extend( - SortableFactory(['id', 'name', 'jobStatus']), - Searchable, - WithNamespaceResetting -) { + SortableFactory(['id', 'name', 'jobStatus']), + Searchable, + WithNamespaceResetting + ) { queryParams = [ { currentPage: 'page', diff --git a/ui/tests/pages/components/clients.js b/ui/tests/pages/components/clients.js index 3a6e31e098ae..4be7fbee31cb 100644 --- a/ui/tests/pages/components/clients.js +++ b/ui/tests/pages/components/clients.js @@ -1,4 +1,4 @@ -import { attribute, collection, clickable, hasClass, text } from 'ember-cli-page-object'; +import { attribute, collection, clickable, text } from 'ember-cli-page-object'; import { singularize } from 'ember-inflector'; export default function(selector = '[data-test-client]', propKey = 'clients') { From d19b8d7ad7470c4831ec488a0b36c2a87a5d9770 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 30 Sep 2021 11:00:13 -0400 Subject: [PATCH 40/58] ui: add help tooltip to summary bar legend --- ui/app/components/distribution-bar.js | 3 ++- ui/app/components/job-client-status-bar.js | 8 ++++++ ui/app/styles/charts/distribution-bar.scss | 26 ++++++++++++++++--- .../components/job-client-status-row.hbs | 4 +-- .../job-client-status-summary-legend-item.hbs | 3 --- .../parts/job-client-status-summary.hbs | 4 +-- .../job-page/parts/summary-legend-item.hbs | 12 +++++++++ .../components/job-page/parts/summary.hbs | 7 +---- 8 files changed, 49 insertions(+), 18 deletions(-) delete mode 100644 ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs create mode 100644 ui/app/templates/components/job-page/parts/summary-legend-item.hbs diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index 6e9fd8c4dbaa..369380103212 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -34,12 +34,13 @@ export default class DistributionBar extends Component.extend(WindowResizable) { const data = copy(this.data, true); const sum = data.mapBy('value').reduce(sumAggregate, 0); - return data.map(({ label, value, className, layers, queryParams }, index) => ({ + return data.map(({ label, value, className, layers, queryParams, help }, index) => ({ label, value, className, layers, queryParams, + help, index, percent: value / sum, offset: diff --git a/ui/app/components/job-client-status-bar.js b/ui/app/components/job-client-status-bar.js index 52d4c44f2cc8..67583439bf82 100644 --- a/ui/app/components/job-client-status-bar.js +++ b/ui/app/components/job-client-status-bar.js @@ -29,6 +29,7 @@ export default class JobClientStatusBar extends DistributionBar { value: queued.length, className: 'queued', queryParams: { status: JSON.stringify(['queued']) }, + help: 'Job registered but waiting to be scheduled into these clients.', }, { label: 'Starting', @@ -36,42 +37,49 @@ export default class JobClientStatusBar extends DistributionBar { className: 'starting', queryParams: { status: JSON.stringify(['starting']) }, layers: 2, + help: 'Job scheduled but all allocations are pending in these clients.', }, { label: 'Running', value: running.length, className: 'running', queryParams: { status: JSON.stringify(['running']) }, + help: 'At least one allocation for this job is running in these clients.', }, { label: 'Complete', value: complete.length, className: 'complete', queryParams: { status: JSON.stringify(['complete']) }, + help: 'All allocations for this job have completed successfully in these clients.', }, { label: 'Degraded', value: degraded.length, className: 'degraded', queryParams: { status: JSON.stringify(['degraded']) }, + help: 'Some allocations for this job were not successfull or did not run.', }, { label: 'Failed', value: failed.length, className: 'failed', queryParams: { status: JSON.stringify(['failed']) }, + help: 'At least one allocation for this job has failed in these clients.', }, { label: 'Lost', value: lost.length, className: 'lost', queryParams: { status: JSON.stringify(['lost']) }, + help: 'At least one allocation for this job was lost in these clients.', }, { label: 'Not Scheduled', value: notScheduled.length, className: 'not-scheduled', queryParams: { status: JSON.stringify(['notScheduled']) }, + help: 'No allocations for this job were scheduled into these clients.', }, ]; } diff --git a/ui/app/styles/charts/distribution-bar.scss b/ui/app/styles/charts/distribution-bar.scss index edafacfdec8b..507f8bdb93f7 100644 --- a/ui/app/styles/charts/distribution-bar.scss +++ b/ui/app/styles/charts/distribution-bar.scss @@ -67,10 +67,28 @@ // Ensure two columns, but don't use the full width width: 35%; - .label, - .value { - display: inline; - font-weight: $weight-normal; + .legend-item { + display: flex; + align-items: center; + + .color-swatch { + margin-right: 0.5rem; + } + + .text { + flex-grow: 1; + + .label, + .value { + display: inline; + font-weight: $weight-normal; + } + } + + .icon { + width: 1.2rem; + height: 1.2rem; + } } &.is-active { diff --git a/ui/app/templates/components/job-client-status-row.hbs b/ui/app/templates/components/job-client-status-row.hbs index e815ccf4b087..08540a00c270 100644 --- a/ui/app/templates/components/job-client-status-row.hbs +++ b/ui/app/templates/components/job-client-status-row.hbs @@ -9,7 +9,7 @@ {{#if this.row.createTime}} - + {{moment-from-now this.row.createTime}} {{else}} @@ -18,7 +18,7 @@ {{#if this.row.modifyTime}} - + {{moment-from-now this.row.modifyTime}} {{else}} diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs deleted file mode 100644 index a7c591b57fa7..000000000000 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary-legend-item.hbs +++ /dev/null @@ -1,3 +0,0 @@ - -{{@datum.value}} -{{@datum.label}} diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index b258685eea08..ff14af4f3dec 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -37,10 +37,10 @@
    3. {{#if (gt datum.value 0)}} - + {{else}} - + {{/if}}
    4. {{/each}} diff --git a/ui/app/templates/components/job-page/parts/summary-legend-item.hbs b/ui/app/templates/components/job-page/parts/summary-legend-item.hbs new file mode 100644 index 000000000000..ab7997fbe6c5 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/summary-legend-item.hbs @@ -0,0 +1,12 @@ +
      + + + {{@datum.value}} + {{@datum.label}} + + {{#if @datum.help}} + + {{x-icon "info-circle-outline" class="is-faded"}} + + {{/if}} +
      diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index e90cdff53753..244e4f7d329e 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -40,15 +40,10 @@
        {{#each chart.data as |datum index|}}
      1. - - {{datum.value}} - - {{datum.label}} - +
      2. {{/each}}
      {{/component}} - From 807a3ebffe487a5b6ba952c57271544fed15867e Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 4 Oct 2021 14:45:57 -0400 Subject: [PATCH 41/58] ui: add sysbatch as a job list filter option --- ui/app/controllers/jobs/index.js | 1 + ui/tests/acceptance/jobs-list-test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/controllers/jobs/index.js b/ui/app/controllers/jobs/index.js index 12c05fbd1f9a..4dcf47d62271 100644 --- a/ui/app/controllers/jobs/index.js +++ b/ui/app/controllers/jobs/index.js @@ -83,6 +83,7 @@ export default class IndexController extends Controller.extend(Sortable, Searcha { key: 'periodic', label: 'Periodic' }, { key: 'service', label: 'Service' }, { key: 'system', label: 'System' }, + { key: 'sysbatch', label: 'System Batch' }, ]; } diff --git a/ui/tests/acceptance/jobs-list-test.js b/ui/tests/acceptance/jobs-list-test.js index 5e5c2052e9c0..ada334696460 100644 --- a/ui/tests/acceptance/jobs-list-test.js +++ b/ui/tests/acceptance/jobs-list-test.js @@ -217,7 +217,7 @@ module('Acceptance | jobs list', function(hooks) { testFacet('Type', { facet: JobsList.facets.type, paramName: 'type', - expectedOptions: ['Batch', 'Parameterized', 'Periodic', 'Service', 'System'], + expectedOptions: ['Batch', 'Parameterized', 'Periodic', 'Service', 'System', 'System Batch'], async beforeEach() { server.createList('job', 2, { createAllocations: false, type: 'batch' }); server.createList('job', 2, { From f80d97f7671530d5e8d5151dcfa25906b0d27c10 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 4 Oct 2021 14:51:25 -0400 Subject: [PATCH 42/58] ui: add parameterized and periodic sysbatch jobs to mirage --- ui/mirage/factories/job.js | 71 ++++++++++++++++++++++++++++++++-- ui/mirage/scenarios/default.js | 2 + 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index ce57711580b4..747b01b31bf7 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -67,6 +67,20 @@ export default Factory.extend({ }), }), + periodicSysbatch: trait({ + type: 'sysbatch', + 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, @@ -79,6 +93,18 @@ export default Factory.extend({ }), }), + parameterizedSysbatch: trait({ + type: 'sysbatch', + parameterized: true, + // parameterized job object + // serializer update for bool vs details object + parameterizedJob: () => ({ + MetaOptional: generateMetaFields(faker.random.number(10), 'optional'), + MetaRequired: generateMetaFields(faker.random.number(10), 'required'), + Payload: faker.random.boolean() ? 'required' : null, + }), + }), + periodicChild: trait({ // Periodic children need a parent job, // It is the Periodic job's responsibility to create @@ -86,6 +112,13 @@ export default Factory.extend({ type: 'batch', }), + periodicSysbatchChild: trait({ + // Periodic children need a parent job, + // It is the Periodic job's responsibility to create + // periodicChild jobs and provide a parent job. + type: 'sysbatch', + }), + parameterizedChild: trait({ // Parameterized children need a parent job, // It is the Parameterized job's responsibility to create @@ -96,6 +129,16 @@ export default Factory.extend({ payload: window.btoa(faker.lorem.sentence()), }), + parameterizedSysbatchChild: trait({ + // Parameterized children need a parent job, + // It is the Parameterized job's responsibility to create + // parameterizedChild jobs and provide a parent job. + type: 'sysbatch', + parameterized: true, + dispatched: true, + payload: window.btoa(faker.lorem.sentence()), + }), + createIndex: i => i, modifyIndex: () => faker.random.number({ min: 10, max: 2000 }), @@ -248,8 +291,18 @@ export default Factory.extend({ } if (job.periodic) { - // Create periodicChild jobs - server.createList('job', job.childrenCount, 'periodicChild', { + let childType; + switch (job.type) { + case 'batch': + childType = 'periodicChild'; + break; + case 'sysbatch': + childType = 'periodicSysbatchChild'; + break; + } + + // Create child jobs + server.createList('job', job.childrenCount, childType, { parentId: job.id, namespaceId: job.namespaceId, namespace: job.namespace, @@ -259,8 +312,18 @@ export default Factory.extend({ } if (job.parameterized && !job.parentId) { - // Create parameterizedChild jobs - server.createList('job', job.childrenCount, 'parameterizedChild', { + let childType; + switch (job.type) { + case 'batch': + childType = 'parameterizedChild'; + break; + case 'sysbatch': + childType = 'parameterizedSysbatchChild'; + break; + } + + // Create child jobs + server.createList('job', job.childrenCount, childType, { parentId: job.id, namespaceId: job.namespaceId, namespace: job.namespace, diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js index 1128b7a884aa..6e5d11c123bf 100644 --- a/ui/mirage/scenarios/default.js +++ b/ui/mirage/scenarios/default.js @@ -87,6 +87,8 @@ function allJobTypes(server) { server.create('job', { type: 'system' }); server.create('job', 'periodic'); server.create('job', 'parameterized'); + server.create('job', 'periodicSysbatch'); + server.create('job', 'parameterizedSysbatch'); server.create('job', { failedPlacements: true }); } From d5d069ada2557e898c2523e495fb72d6f99e595d Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 4 Oct 2021 18:23:16 -0400 Subject: [PATCH 43/58] ui: fix merge --- ui/app/routes/jobs/job/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index bceb1539880b..463edee01010 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -28,7 +28,7 @@ export default class IndexRoute extends Route.extend(WithWatchers) { model.get('supportsDeployments') && this.watchLatestDeployment.perform(model), list: model.get('hasChildren') && - this.watchAll.perform({ namespace: model.namespace.get('name') }), + this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), nodes: model.get('hasClientStatus') && this.watchNodes.perform(), }); } From 967c350e29ba09a8ddd72b7ca1a2882e5a4a488d Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 4 Oct 2021 19:03:27 -0400 Subject: [PATCH 44/58] ui: force namespace query param in job details tabs --- ui/app/templates/components/job-subnav.hbs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index 62cecb3f58a8..715277942e53 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -1,15 +1,15 @@
        -
      • Overview
      • -
      • Definition
      • -
      • Versions
      • +
      • Overview
      • +
      • Definition
      • +
      • Versions
      • {{#if this.job.supportsDeployments}} -
      • Deployments
      • +
      • Deployments
      • {{/if}} -
      • Allocations
      • -
      • Evaluations
      • +
      • Allocations
      • +
      • Evaluations
      • {{#if this.job.hasClientStatus}} -
      • Clients
      • +
      • Clients
      • {{/if}}
      From d6932bd4eae5592ce584e3b775bece8ca756c0f7 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 4 Oct 2021 20:13:03 -0400 Subject: [PATCH 45/58] ui: filter out old allocations when computing job status in client --- ui/app/utils/properties/job-client-status.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index 1988d33d7567..d017b82d34a1 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -100,14 +100,16 @@ function jobStatus(allocs, expected) { } // Count how many allocations are in each `clientStatus` value. - const summary = allocs.reduce((acc, a) => { - const status = a.clientStatus; - if (!acc[status]) { - acc[status] = 0; - } - acc[status]++; - return acc; - }, {}); + const summary = allocs + .filter(a => !a.isOld) + .reduce((acc, a) => { + const status = a.clientStatus; + if (!acc[status]) { + acc[status] = 0; + } + acc[status]++; + return acc; + }, {}); // Theses statuses are considered terminal, i.e., an allocation will never // move from this status to another. From 05e5f599bf5cb65b248f1c654c8c6d0f7dafc528 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 4 Oct 2021 20:20:53 -0400 Subject: [PATCH 46/58] ui: remove some of the redundant help messages for job status in client --- ui/app/components/job-client-status-bar.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/app/components/job-client-status-bar.js b/ui/app/components/job-client-status-bar.js index 67583439bf82..53ec76fd3ac9 100644 --- a/ui/app/components/job-client-status-bar.js +++ b/ui/app/components/job-client-status-bar.js @@ -29,7 +29,6 @@ export default class JobClientStatusBar extends DistributionBar { value: queued.length, className: 'queued', queryParams: { status: JSON.stringify(['queued']) }, - help: 'Job registered but waiting to be scheduled into these clients.', }, { label: 'Starting', @@ -37,21 +36,18 @@ export default class JobClientStatusBar extends DistributionBar { className: 'starting', queryParams: { status: JSON.stringify(['starting']) }, layers: 2, - help: 'Job scheduled but all allocations are pending in these clients.', }, { label: 'Running', value: running.length, className: 'running', queryParams: { status: JSON.stringify(['running']) }, - help: 'At least one allocation for this job is running in these clients.', }, { label: 'Complete', value: complete.length, className: 'complete', queryParams: { status: JSON.stringify(['complete']) }, - help: 'All allocations for this job have completed successfully in these clients.', }, { label: 'Degraded', @@ -65,14 +61,12 @@ export default class JobClientStatusBar extends DistributionBar { value: failed.length, className: 'failed', queryParams: { status: JSON.stringify(['failed']) }, - help: 'At least one allocation for this job has failed in these clients.', }, { label: 'Lost', value: lost.length, className: 'lost', queryParams: { status: JSON.stringify(['lost']) }, - help: 'At least one allocation for this job was lost in these clients.', }, { label: 'Not Scheduled', From fa0d48a12471311e193833925c3693bd458752c3 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 5 Oct 2021 13:22:13 -0400 Subject: [PATCH 47/58] ui: fix distribution bar click event listener --- ui/app/components/distribution-bar.js | 10 +++++----- ui/app/components/job-client-status-bar.js | 1 - .../job-page/parts/job-client-status-summary.hbs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index 369380103212..f2c40e9a0b42 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -22,7 +22,7 @@ const sumAggregate = (total, val) => total + val; export default class DistributionBar extends Component.extend(WindowResizable) { chart = null; @overridable(() => null) data; - @overridable(() => null) onSliceClick; + onSliceClick = null; activeDatum = null; isNarrow = false; @@ -97,10 +97,6 @@ export default class DistributionBar extends Component.extend(WindowResizable) { slices.exit().remove(); - if (this.onSliceClick) { - slices.on('click', this.onSliceClick); - } - let slicesEnter = slices.enter() .append('g') .on('mouseenter', d => { @@ -185,6 +181,10 @@ export default class DistributionBar extends Component.extend(WindowResizable) { .attr('height', '6px') .attr('y', '50%'); } + + if (this.onSliceClick) { + slices.on('click', this.onSliceClick); + } } /* eslint-enable */ diff --git a/ui/app/components/job-client-status-bar.js b/ui/app/components/job-client-status-bar.js index 53ec76fd3ac9..6d8888ba6795 100644 --- a/ui/app/components/job-client-status-bar.js +++ b/ui/app/components/job-client-status-bar.js @@ -8,7 +8,6 @@ export default class JobClientStatusBar extends DistributionBar { 'data-test-job-client-status-bar' = true; jobClientStatus = null; - onSliceClick() {} @computed('jobClientStatus.byStatus') get data() { diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index ff14af4f3dec..01db9c7fdf2b 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -17,7 +17,7 @@
      From a8cd04aafd2d5103a89fff324bc3dae9bf501a45 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Tue, 5 Oct 2021 15:09:35 -0400 Subject: [PATCH 48/58] edited dependent keys in job client status helper The job client status helper relies on updating whenever an allocation client status updates. Or if a new allocation is added to the client node. We were previously observing the allocations array as a whole, but we're depending on a specific property in that list to update in order to make sure we're getting notifications to update at the right time. --- ui/app/utils/properties/job-client-status.js | 80 ++++++++++--------- .../components/job-client-status-bar-test.js | 50 ++++++------ 2 files changed, 67 insertions(+), 63 deletions(-) diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index d017b82d34a1..46103f78c730 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -16,46 +16,50 @@ const STATUS = [ // // ex. clientStaus: jobClientStatus('nodes', 'job'), export default function jobClientStatus(nodesKey, jobKey) { - return computed(nodesKey, `${jobKey}.{datacenters,status,allocations,taskGroups}`, function() { - const job = this.get(jobKey); - const nodes = this.get(nodesKey); - - // Filter nodes by the datacenters defined in the job. - const filteredNodes = nodes.filter(n => { - return job.datacenters.indexOf(n.datacenter) >= 0; - }); + return computed( + nodesKey, + `${jobKey}.{datacenters,status,allocations.@each.clientStatus,taskGroups}`, + function() { + const job = this.get(jobKey); + const nodes = this.get(nodesKey); + + // Filter nodes by the datacenters defined in the job. + const filteredNodes = nodes.filter(n => { + return job.datacenters.indexOf(n.datacenter) >= 0; + }); + + if (job.status === 'pending') { + return allQueued(filteredNodes); + } - if (job.status === 'pending') { - return allQueued(filteredNodes); + // Group the job allocations by the ID of the client that is running them. + const allocsByNodeID = {}; + job.allocations.forEach(a => { + const nodeId = a.node.get('id'); + if (!allocsByNodeID[nodeId]) { + allocsByNodeID[nodeId] = []; + } + allocsByNodeID[nodeId].push(a); + }); + + const result = { + byNode: {}, + byStatus: {}, + totalNodes: filteredNodes.length, + }; + filteredNodes.forEach(n => { + const status = jobStatus(allocsByNodeID[n.id], job.taskGroups.length); + result.byNode[n.id] = status; + + if (!result.byStatus[status]) { + result.byStatus[status] = []; + } + result.byStatus[status].push(n.id); + }); + result.byStatus = canonicalizeStatus(result.byStatus); + return result; } - - // Group the job allocations by the ID of the client that is running them. - const allocsByNodeID = {}; - job.allocations.forEach(a => { - const nodeId = a.node.get('id'); - if (!allocsByNodeID[nodeId]) { - allocsByNodeID[nodeId] = []; - } - allocsByNodeID[nodeId].push(a); - }); - - const result = { - byNode: {}, - byStatus: {}, - totalNodes: filteredNodes.length, - }; - filteredNodes.forEach(n => { - const status = jobStatus(allocsByNodeID[n.id], job.taskGroups.length); - result.byNode[n.id] = status; - - if (!result.byStatus[status]) { - result.byStatus[status] = []; - } - result.byStatus[status].push(n.id); - }); - result.byStatus = canonicalizeStatus(result.byStatus); - return result; - }); + ); } function allQueued(nodes) { diff --git a/ui/tests/integration/components/job-client-status-bar-test.js b/ui/tests/integration/components/job-client-status-bar-test.js index 3abe86248e38..729c2cc7b7d3 100644 --- a/ui/tests/integration/components/job-client-status-bar-test.js +++ b/ui/tests/integration/components/job-client-status-bar-test.js @@ -13,7 +13,7 @@ module('Integration | Component | job-client-status-bar', function(hooks) { setupRenderingTest(hooks); const commonProperties = () => ({ - onBarClick: sinon.spy(), + onSliceClick: sinon.spy(), jobClientStatus: { byStatus: { queued: [], @@ -31,7 +31,7 @@ module('Integration | Component | job-client-status-bar', function(hooks) { const commonTemplate = hbs` `; @@ -46,27 +46,27 @@ module('Integration | Component | job-client-status-bar', function(hooks) { }); // TODO: fix tests for slice click - // test('it fires the onBarClick handler method when clicking a bar in the chart', async function(assert) { - // const props = commonProperties(); - // this.setProperties(props); - // await render(commonTemplate); - // await JobClientStatusBar.slices[0].click(); - // assert.ok(props.onBarClick.calledOnce); - // }); - // - // test('it handles an update to client status property', async function(assert) { - // const props = commonProperties(); - // this.setProperties(props); - // await render(commonTemplate); - // const newProps = { - // ...props, - // jobClientStatus: { - // ...props.jobClientStatus, - // byStatus: { ...props.jobClientStatus.byStatus, starting: [], running: ['someNodeId'] }, - // }, - // }; - // this.setProperties(newProps); - // await JobClientStatusBar.visitSlice('running'); - // assert.ok(props.onBarClick.calledOnce); - // }); + test('it fires the onBarClick handler method when clicking a bar in the chart', async function(assert) { + const props = commonProperties(); + this.setProperties(props); + await render(commonTemplate); + await JobClientStatusBar.slices[0].click(); + assert.ok(props.onSliceClick.calledOnce); + }); + + test('it handles an update to client status property', async function(assert) { + const props = commonProperties(); + this.setProperties(props); + await render(commonTemplate); + const newProps = { + ...props, + jobClientStatus: { + ...props.jobClientStatus, + byStatus: { ...props.jobClientStatus.byStatus, starting: [], running: ['someNodeId'] }, + }, + }; + this.setProperties(newProps); + await JobClientStatusBar.visitSlice('running'); + assert.ok(props.onSliceClick.calledOnce); + }); }); From 86a1e14bc54bc312051c2d0bdf37a426fbfa56eb Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 6 Oct 2021 14:13:53 -0400 Subject: [PATCH 49/58] edit observable for node in job client status --- ui/app/utils/properties/job-client-status.js | 2 +- ui/tests/integration/components/job-client-status-bar-test.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index 46103f78c730..c6482076c06a 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -17,7 +17,7 @@ const STATUS = [ // ex. clientStaus: jobClientStatus('nodes', 'job'), export default function jobClientStatus(nodesKey, jobKey) { return computed( - nodesKey, + `${nodesKey}.[]`, `${jobKey}.{datacenters,status,allocations.@each.clientStatus,taskGroups}`, function() { const job = this.get(jobKey); diff --git a/ui/tests/integration/components/job-client-status-bar-test.js b/ui/tests/integration/components/job-client-status-bar-test.js index 729c2cc7b7d3..a0b7e39f06ec 100644 --- a/ui/tests/integration/components/job-client-status-bar-test.js +++ b/ui/tests/integration/components/job-client-status-bar-test.js @@ -45,7 +45,6 @@ module('Integration | Component | job-client-status-bar', function(hooks) { await componentA11yAudit(this.element, assert); }); - // TODO: fix tests for slice click test('it fires the onBarClick handler method when clicking a bar in the chart', async function(assert) { const props = commonProperties(); this.setProperties(props); From 91bb5b137cfc9c84caceb95d4e31c8409ee3d42f Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 6 Oct 2021 16:18:48 -0400 Subject: [PATCH 50/58] add conditional rendering logic for child jobs --- .../job-page/parameterized-child.js | 9 ++ ui/app/components/job-page/periodic-child.js | 10 +++ .../job-page/parameterized-child.hbs | 88 ++++++++++++++----- .../components/job-page/periodic-child.hbs | 65 ++++++++++---- 4 files changed, 133 insertions(+), 39 deletions(-) diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js index 3f941067c069..daf0e4183401 100644 --- a/ui/app/components/job-page/parameterized-child.js +++ b/ui/app/components/job-page/parameterized-child.js @@ -1,11 +1,14 @@ import { computed } from '@ember/object'; import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; import PeriodicChildJobPage from './periodic-child'; import classic from 'ember-classic-decorator'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic export default class ParameterizedChild extends PeriodicChildJobPage { @alias('job.decodedPayload') payload; + @service store; @computed('payload') get payloadJSON() { @@ -17,4 +20,10 @@ export default class ParameterizedChild extends PeriodicChildJobPage { } return json; } + + @jobClientStatus('nodes', 'job') jobClientStatus; + + get nodes() { + return this.store.peekAll('node'); + } } diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js index dfe42225dc35..d581d88dc29b 100644 --- a/ui/app/components/job-page/periodic-child.js +++ b/ui/app/components/job-page/periodic-child.js @@ -1,9 +1,13 @@ import AbstractJobPage from './abstract'; import { computed } from '@ember/object'; +import { inject as service } from '@ember/service'; import classic from 'ember-classic-decorator'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic export default class PeriodicChild extends AbstractJobPage { + @service store; + @computed('job.{name,id}', 'job.parent.{name,id}') get breadcrumbs() { const job = this.job; @@ -21,4 +25,10 @@ export default class PeriodicChild extends AbstractJobPage { }, ]; } + + @jobClientStatus('nodes', 'job') jobClientStatus; + + get nodes() { + return this.store.peekAll('node'); + } } diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 881c9af49ae6..78e3ccf03d2f 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -1,36 +1,68 @@ - - - - + +
      - Type: {{this.job.type}} | - Priority: {{this.job.priority}} | + + + Type: + + {{this.job.type}} + | + + + + Priority: + + {{this.job.priority}} + | + - Parent: - + + Parent: + + {{this.job.parent.name}} {{#if (and this.job.namespace this.system.shouldShowNamespaces)}} - | Namespace: {{this.job.namespace.name}} + + | + + Namespace: + + {{this.job.namespace.name}} + {{/if}}
      - - - + {{#if this.job.hasClientStatus}} + + {{/if}} + - - + @gotoTaskGroup={{this.gotoTaskGroup}} + /> -
      Meta @@ -40,26 +72,36 @@ + @class="attributes-table" + />
      {{else}}
      -

      No Meta Attributes

      -

      This job is configured with no meta attributes.

      +

      + No Meta Attributes +

      +

      + This job is configured with no meta attributes. +

      {{/if}}
      -
      -
      Payload
      +
      + Payload +
      {{#if this.payloadJSON}} {{else}} -
      {{this.payload}}
      +
      +          
      +            {{this.payload}}
      +          
      +        
      {{/if}}
      -
      + \ No newline at end of file diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index 3c14a25fc263..162c44ea708b 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -1,33 +1,66 @@ - - - - + +
      - Type: {{this.job.type}} | - Priority: {{this.job.priority}} | + + + Type: + + {{this.job.type}} + | + + + + Priority: + + {{this.job.priority}} + | + - Parent: - + + Parent: + + {{this.job.parent.name}} {{#if (and this.job.namespace this.system.shouldShowNamespaces)}} - | Namespace: {{this.job.namespace.name}} + + | + + Namespace: + + {{this.job.namespace.name}} + {{/if}}
      - - - + {{#if this.job.hasClientStatus}} + + {{/if}} + - - + @gotoTaskGroup={{this.gotoTaskGroup}} + /> -
      + \ No newline at end of file From 620b8d089e3ea4051afc6479c27105a7f3a5fc34 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Wed, 6 Oct 2021 18:53:04 -0400 Subject: [PATCH 51/58] ui: fix job client status summary navigation for jobs with namespace --- ui/app/components/distribution-bar.js | 4 +- ui/app/components/job-client-status-bar.js | 57 ++++++++++++++++--- ui/app/controllers/jobs/job/index.js | 5 +- .../parts/job-client-status-summary.hbs | 4 +- ui/tests/acceptance/job-detail-test.js | 14 +++++ ui/tests/helpers/module-for-job.js | 53 +++++++++++------ 6 files changed, 107 insertions(+), 30 deletions(-) diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index f2c40e9a0b42..50cd5b5c664a 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -34,12 +34,12 @@ export default class DistributionBar extends Component.extend(WindowResizable) { const data = copy(this.data, true); const sum = data.mapBy('value').reduce(sumAggregate, 0); - return data.map(({ label, value, className, layers, queryParams, help }, index) => ({ + return data.map(({ label, value, className, layers, legendLink, help }, index) => ({ label, value, className, layers, - queryParams, + legendLink, help, index, percent: value / sum, diff --git a/ui/app/components/job-client-status-bar.js b/ui/app/components/job-client-status-bar.js index 6d8888ba6795..f5f6621ae6b7 100644 --- a/ui/app/components/job-client-status-bar.js +++ b/ui/app/components/job-client-status-bar.js @@ -7,6 +7,7 @@ export default class JobClientStatusBar extends DistributionBar { layoutName = 'components/distribution-bar'; 'data-test-job-client-status-bar' = true; + job = null; jobClientStatus = null; @computed('jobClientStatus.byStatus') @@ -27,51 +28,91 @@ export default class JobClientStatusBar extends DistributionBar { label: 'Queued', value: queued.length, className: 'queued', - queryParams: { status: JSON.stringify(['queued']) }, + legendLink: { + queryParams: { + status: JSON.stringify(['queued']), + namespace: this.job.namespace.get('id'), + }, + }, }, { label: 'Starting', value: starting.length, className: 'starting', - queryParams: { status: JSON.stringify(['starting']) }, + legendLink: { + queryParams: { + status: JSON.stringify(['starting']), + namespace: this.job.namespace.get('id'), + }, + }, layers: 2, }, { label: 'Running', value: running.length, className: 'running', - queryParams: { status: JSON.stringify(['running']) }, + legendLink: { + queryParams: { + status: JSON.stringify(['running']), + namespace: this.job.namespace.get('id'), + }, + }, }, { label: 'Complete', value: complete.length, className: 'complete', - queryParams: { status: JSON.stringify(['complete']) }, + legendLink: { + queryParams: { + status: JSON.stringify(['complete']), + namespace: this.job.namespace.get('id'), + }, + }, }, { label: 'Degraded', value: degraded.length, className: 'degraded', - queryParams: { status: JSON.stringify(['degraded']) }, + legendLink: { + queryParams: { + status: JSON.stringify(['degraded']), + namespace: this.job.namespace.get('id'), + }, + }, help: 'Some allocations for this job were not successfull or did not run.', }, { label: 'Failed', value: failed.length, className: 'failed', - queryParams: { status: JSON.stringify(['failed']) }, + legendLink: { + queryParams: { + status: JSON.stringify(['failed']), + namespace: this.job.namespace.get('id'), + }, + }, }, { label: 'Lost', value: lost.length, className: 'lost', - queryParams: { status: JSON.stringify(['lost']) }, + legendLink: { + queryParams: { + status: JSON.stringify(['lost']), + namespace: this.job.namespace.get('id'), + }, + }, }, { label: 'Not Scheduled', value: notScheduled.length, className: 'not-scheduled', - queryParams: { status: JSON.stringify(['notScheduled']) }, + legendLink: { + queryParams: { + status: JSON.stringify(['notScheduled']), + namespace: this.job.namespace.get('id'), + }, + }, help: 'No allocations for this job were scheduled into these clients.', }, ]; diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 3bbcdeed184e..2b663959f97e 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -43,7 +43,10 @@ export default class IndexController extends Controller.extend(WithNamespaceRese @action gotoClients(statusFilter) { this.transitionToRoute('jobs.job.clients', this.job, { - queryParams: { status: JSON.stringify(statusFilter) }, + queryParams: { + status: JSON.stringify(statusFilter), + namespace: this.job.get('namespace.name'), + }, }); } } diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index 01db9c7fdf2b..c96d0ff4e4e2 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -18,6 +18,7 @@
      @@ -29,6 +30,7 @@ @@ -36,7 +38,7 @@ {{#each chart.data as |datum index|}}
    5. {{#if (gt datum.value 0)}} - + {{else}} diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index 64f39458c1d2..5a680826c14d 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -38,6 +38,20 @@ moduleForJobWithClientStatus('Acceptance | job detail with client status (sysbat }) ); +moduleForJobWithClientStatus( + 'Acceptance | job detail with client status (sysbatch with namespace)', + () => { + const namespace = server.create('namespace', { id: 'test' }); + return server.create('job', { + status: 'running', + datacenters: ['dc1'], + type: 'sysbatch', + namespaceId: namespace.name, + createAllocations: false, + }); + } +); + moduleForJob( 'Acceptance | job detail (periodic)', 'children', diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 785fbda327f8..bec37cc51e1e 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -136,7 +136,19 @@ export function moduleForJobWithClientStatus(title, jobFactory, additionalTests) clients.forEach(c => { server.create('allocation', { jobId: job.id, nodeId: c.id }); }); - await JobDetail.visit({ id: job.id }); + if (!job.namespace || job.namespace === 'default') { + await JobDetail.visit({ id: job.id }); + } else { + await JobDetail.visit({ id: job.id, namespace: job.namespace }); + } + }); + + test('the subnav links to clients', async function(assert) { + await JobDetail.tabFor('clients').visit(); + assert.equal( + currentURL(), + urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}/clients`, job.namespace) + ); }); test('job status summary is shown in the overview', async function(assert) { @@ -148,28 +160,33 @@ export function moduleForJobWithClientStatus(title, jobFactory, additionalTests) test('clicking legend item navigates to a pre-filtered clients table', async function(assert) { const legendItem = JobDetail.jobClientStatusSummary.legend.clickableItems[0]; - const encodedStatus = encodeURIComponent(JSON.stringify([legendItem.label])); + const status = legendItem.label; await legendItem.click(); - assert.equal( - currentURL(), - `/jobs/${job.name}/clients?status=${encodedStatus}`, - 'Client Status Bar Chart legend links to client tab' + const encodedStatus = encodeURIComponent(JSON.stringify([status])); + const expectedURL = new URL( + urlWithNamespace(`/jobs/${job.name}/clients?status=${encodedStatus}`, job.namespace), + window.location ); + const gotURL = new URL(currentURL(), window.location); + assert.deepEqual(gotURL.path, expectedURL.path); + assert.deepEqual(gotURL.searchParams, expectedURL.searchParams); }); - // TODO: fix click event on slices during tests - // test('clicking in a slice takes you to a pre-filtered view of clients', async function(assert) { - // const slice = JobDetail.jobClientStatusSummary.slices[0]; - // await slice.click(); - // - // const encodedStatus = encodeURIComponent(JSON.stringify([slice.label])); - // assert.equal( - // currentURL(), - // `/jobs/${job.name}/clients?status=${encodedStatus}`, - // 'Client Status Bar Chart links to client tab' - // ); - // }); + test('clicking in a slice takes you to a pre-filtered clients table', async function(assert) { + const slice = JobDetail.jobClientStatusSummary.slices[0]; + const status = slice.label; + await slice.click(); + + const encodedStatus = encodeURIComponent(JSON.stringify([status])); + const expectedURL = new URL( + urlWithNamespace(`/jobs/${job.name}/clients?status=${encodedStatus}`, job.namespace), + window.location + ); + const gotURL = new URL(currentURL(), window.location); + assert.deepEqual(gotURL.path, expectedURL.path); + assert.deepEqual(gotURL.searchParams, expectedURL.searchParams); + }); for (var testName in additionalTests) { test(testName, async function(assert) { From 7ca852b8a28b6e2d80aa4da6ddc21c9a784100fc Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Wed, 6 Oct 2021 18:56:25 -0400 Subject: [PATCH 52/58] ui: add missing computed property dependency --- ui/app/components/job-client-status-bar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/job-client-status-bar.js b/ui/app/components/job-client-status-bar.js index f5f6621ae6b7..9f3690b01f21 100644 --- a/ui/app/components/job-client-status-bar.js +++ b/ui/app/components/job-client-status-bar.js @@ -10,7 +10,7 @@ export default class JobClientStatusBar extends DistributionBar { job = null; jobClientStatus = null; - @computed('jobClientStatus.byStatus') + @computed('job.namespace', 'jobClientStatus.byStatus') get data() { const { queued, From 452835d32991c95136d03cb1dc68d778715bdebe Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 6 Oct 2021 19:52:02 -0400 Subject: [PATCH 53/58] refactor forceCollapsed logic to use hasClientStatus prop --- ui/app/templates/components/job-page/parameterized-child.hbs | 2 +- ui/app/templates/components/job-page/periodic-child.hbs | 2 +- ui/tests/unit/utils/job-client-status-test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 78e3ccf03d2f..97b97f72eed8 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -54,7 +54,7 @@ @jobClientStatus={{this.jobClientStatus}} /> {{/if}} - + {{/if}} - + Date: Wed, 6 Oct 2021 19:15:59 -0400 Subject: [PATCH 54/58] ui: revert automated code formating --- .../job-page/parameterized-child.hbs | 78 ++++++------------- .../components/job-page/periodic-child.hbs | 55 ++++--------- 2 files changed, 37 insertions(+), 96 deletions(-) diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 97b97f72eed8..1c22c62e2bdf 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -1,52 +1,24 @@ - - + + + +
      - - - Type: - - {{this.job.type}} - | - - - - Priority: - - {{this.job.priority}} - | - + Type: {{this.job.type}} | + Priority: {{this.job.priority}} | - - Parent: - - + Parent: + {{this.job.parent.name}} {{#if (and this.job.namespace this.system.shouldShowNamespaces)}} - - | - - Namespace: - - {{this.job.namespace.name}} - + | Namespace: {{this.job.namespace.name}} {{/if}}
      + {{#if this.job.hasClientStatus}} {{/if}} + + + + @gotoTaskGroup={{this.gotoTaskGroup}} /> + +
      Meta @@ -72,35 +48,25 @@ + @class="attributes-table" />
      {{else}}
      -

      - No Meta Attributes -

      -

      - This job is configured with no meta attributes. -

      +

      No Meta Attributes

      +

      This job is configured with no meta attributes.

      {{/if}}
      +
      -
      - Payload -
      +
      Payload
      {{#if this.payloadJSON}} {{else}} -
      -          
      -            {{this.payload}}
      -          
      -        
      +
      {{this.payload}}
      {{/if}}
      diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index 6d6bd8b5300d..de68904dc2fc 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -1,52 +1,24 @@ - - + + + +
      - - - Type: - - {{this.job.type}} - | - - - - Priority: - - {{this.job.priority}} - | - + Type: {{this.job.type}} | + Priority: {{this.job.priority}} | - - Parent: - - + Parent: + {{this.job.parent.name}} {{#if (and this.job.namespace this.system.shouldShowNamespaces)}} - - | - - Namespace: - - {{this.job.namespace.name}} - + | Namespace: {{this.job.namespace.name}} {{/if}}
      + {{#if this.job.hasClientStatus}} {{/if}} + + + + @gotoTaskGroup={{this.gotoTaskGroup}} /> +
      \ No newline at end of file From c38c0a56151e4d83521f38b79e3605b92e703bd8 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Wed, 6 Oct 2021 20:28:29 -0400 Subject: [PATCH 55/58] ui: small fixes for job client status --- .../components/job-page/parts/job-client-status-summary.hbs | 2 +- ui/app/templates/components/job-subnav.hbs | 2 +- ui/app/templates/jobs/job/clients.hbs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index c96d0ff4e4e2..eab69e8f9363 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -29,7 +29,7 @@ Allocations
    6. Evaluations
    7. - {{#if this.job.hasClientStatus}} + {{#if (and this.job.hasClientStatus (not this.job.hasChildren))}}
    8. Clients
    9. {{/if}} diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index 8efb22eb3cc0..b61d6dacfe82 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -98,7 +98,7 @@ No Clients

      - No clients have been placed. + No clients available.

      From dc22c75ddb5a3a7519f58bd064ef2839c190d5f1 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 7 Oct 2021 11:35:36 -0400 Subject: [PATCH 56/58] ui: fix job-client-status bar tests --- .../integration/components/job-client-status-bar-test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/tests/integration/components/job-client-status-bar-test.js b/ui/tests/integration/components/job-client-status-bar-test.js index a0b7e39f06ec..827c033c70d1 100644 --- a/ui/tests/integration/components/job-client-status-bar-test.js +++ b/ui/tests/integration/components/job-client-status-bar-test.js @@ -14,6 +14,11 @@ module('Integration | Component | job-client-status-bar', function(hooks) { const commonProperties = () => ({ onSliceClick: sinon.spy(), + job: { + namespace: { + get: () => 'my-namespace', + }, + }, jobClientStatus: { byStatus: { queued: [], @@ -32,6 +37,7 @@ module('Integration | Component | job-client-status-bar', function(hooks) { const commonTemplate = hbs` `; From 0dbf4a0a1edc09df385e5bed36679008e4bb1854 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 7 Oct 2021 12:33:39 -0400 Subject: [PATCH 57/58] ui: fix child job client status slice click --- .../job-page/parameterized-child.hbs | 2 +- .../components/job-page/periodic-child.hbs | 2 +- ui/mirage/factories/job.js | 2 ++ ui/tests/acceptance/job-detail-test.js | 32 +++++++++++++++++++ ui/tests/helpers/module-for-job.js | 8 +++-- 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 1c22c62e2bdf..5a4777057c88 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -21,7 +21,7 @@ {{#if this.job.hasClientStatus}} diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index de68904dc2fc..965c5a41a76f 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -21,7 +21,7 @@ {{#if this.job.hasClientStatus}} diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index 747b01b31bf7..bf135888f0c7 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -306,6 +306,7 @@ export default Factory.extend({ parentId: job.id, namespaceId: job.namespaceId, namespace: job.namespace, + datacenters: job.datacenters, createAllocations: job.createAllocations, shallow: job.shallow, }); @@ -327,6 +328,7 @@ export default Factory.extend({ parentId: job.id, namespaceId: job.namespaceId, namespace: job.namespace, + datacenters: job.datacenters, createAllocations: job.createAllocations, shallow: job.shallow, }); diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index 5a680826c14d..6ddc145ebb63 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -52,6 +52,38 @@ moduleForJobWithClientStatus( } ); +moduleForJob('Acceptance | job detail (sysbatch child)', 'allocations', () => { + const parent = server.create('job', 'periodicSysbatch', { + childrenCount: 1, + shallow: true, + datacenters: ['dc1'], + }); + return server.db.jobs.where({ parentId: parent.id })[0]; +}); + +moduleForJobWithClientStatus('Acceptance | job detail with client status (sysbatch child)', () => { + const parent = server.create('job', 'periodicSysbatch', { + childrenCount: 1, + shallow: true, + datacenters: ['dc1'], + }); + return server.db.jobs.where({ parentId: parent.id })[0]; +}); + +moduleForJobWithClientStatus( + 'Acceptance | job detail with client status (sysbatch child with namespace)', + () => { + const namespace = server.create('namespace', { id: 'test' }); + const parent = server.create('job', 'periodicSysbatch', { + childrenCount: 1, + shallow: true, + namespaceId: namespace.name, + datacenters: ['dc1'], + }); + return server.db.jobs.where({ parentId: parent.id })[0]; + } +); + moduleForJob( 'Acceptance | job detail (periodic)', 'children', diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index bec37cc51e1e..d631a374ca7f 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -184,8 +184,12 @@ export function moduleForJobWithClientStatus(title, jobFactory, additionalTests) window.location ); const gotURL = new URL(currentURL(), window.location); - assert.deepEqual(gotURL.path, expectedURL.path); - assert.deepEqual(gotURL.searchParams, expectedURL.searchParams); + assert.deepEqual(gotURL.pathname, expectedURL.pathname); + + // Sort and compare URL query params. + gotURL.searchParams.sort(); + expectedURL.searchParams.sort(); + assert.equal(gotURL.searchParams.toString(), expectedURL.searchParams.toString()); }); for (var testName in additionalTests) { From 8c8c3fdfd34ef25eaae0bd061b0fecd44f934d65 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 7 Oct 2021 16:33:16 -0400 Subject: [PATCH 58/58] ui: add job client status help message --- ui/app/styles/components/accordion.scss | 5 +++++ .../components/job-page/parts/job-client-status-summary.hbs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/ui/app/styles/components/accordion.scss b/ui/app/styles/components/accordion.scss index 2b59864a2cda..7f21d5d83db6 100644 --- a/ui/app/styles/components/accordion.scss +++ b/ui/app/styles/components/accordion.scss @@ -33,6 +33,11 @@ .accordion-head-content { width: 100%; margin-right: 1.5em; + + .tooltip { + margin-left: 0.5rem; + margin-right: 0.5rem; + } } .accordion-toggle { diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index eab69e8f9363..837a6c34a0f5 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -12,6 +12,9 @@ {{this.jobClientStatus.totalNodes}} + + {{x-icon "info-circle-outline" class="is-faded"}} +
      {{#unless a.isOpen}}