From 74bc21e26b1191be48b9a4cb1947c39cd1909ea2 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 24 Jan 2022 10:58:28 -0500 Subject: [PATCH 01/37] ui: prepare rebase for contextual job-page --- ui/app/abilities/client.js | 33 ++----- ui/app/components/job-page/abstract.js | 5 -- .../job-page/parameterized-child.js | 9 ++ .../parts/job-client-status-summary.js | 10 +-- ui/app/components/job-page/periodic-child.js | 10 +++ ui/app/components/job-page/sysbatch.js | 12 ++- ui/app/components/job-page/system.js | 12 ++- .../list-accordion/accordion-head.js | 2 - ui/app/controllers/jobs/job/index.js | 12 +-- ui/app/models/allocation.js | 8 -- ui/app/routes/jobs/job/index.js | 49 +++-------- .../templates/components/allocation-row.hbs | 34 +++----- .../job-page/parameterized-child.hbs | 8 +- .../parts/job-client-status-summary.hbs | 86 +++++++++---------- .../components/job-page/periodic-child.hbs | 8 +- .../components/job-page/sysbatch.hbs | 5 +- .../templates/components/job-page/system.hbs | 5 +- ui/app/templates/components/job-subnav.hbs | 2 +- .../list-accordion/accordion-head.hbs | 4 +- ui/app/templates/components/task-row.hbs | 12 ++- ui/app/templates/jobs/job/index.hbs | 7 +- ui/app/utils/properties/job-client-status.js | 2 +- ui/mirage/factories/allocation.js | 55 ++++++++---- ui/tests/acceptance/allocation-detail-test.js | 50 ----------- ui/tests/acceptance/task-group-detail-test.js | 23 +---- ui/tests/helpers/module-for-job.js | 62 ++----------- .../components/allocation-row-test.js | 20 ----- ui/tests/pages/jobs/detail.js | 18 ++-- ui/tests/unit/abilities/client-test.js | 31 ++----- ui/tests/unit/utils/job-client-status-test.js | 54 ++++-------- 30 files changed, 222 insertions(+), 426 deletions(-) diff --git a/ui/app/abilities/client.js b/ui/app/abilities/client.js index cd1fc8ed1f7..465f1b4a131 100644 --- a/ui/app/abilities/client.js +++ b/ui/app/abilities/client.js @@ -7,9 +7,6 @@ import classic from 'ember-classic-decorator'; export default class Client extends AbstractAbility { // Map abilities to policy options (which are coarse for nodes) // instead of specific behaviors. - @or('bypassAuthorization', 'selfTokenIsManagement', 'policiesIncludeNodeRead') - canRead; - @or( 'bypassAuthorization', 'selfTokenIsManagement', @@ -17,29 +14,15 @@ export default class Client extends AbstractAbility { ) canWrite; - @computed('token.selfTokenPolicies.[]') - get policiesIncludeNodeRead() { - return policiesIncludePermissions(this.get('token.selfTokenPolicies'), [ - 'read', - 'write', - ]); - } - @computed('token.selfTokenPolicies.[]') get policiesIncludeNodeWrite() { - return policiesIncludePermissions(this.get('token.selfTokenPolicies'), [ - 'write', - ]); - } -} + // For each policy record, extract the Node policy + const policies = (this.get('token.selfTokenPolicies') || []) + .toArray() + .map((policy) => get(policy, 'rulesJSON.Node.Policy')) + .compact(); -function policiesIncludePermissions(policies = [], permissions = []) { - // For each policy record, extract the Node policy - const nodePolicies = policies - .toArray() - .map((policy) => get(policy, 'rulesJSON.Node.Policy')) - .compact(); - - // Check for requested permissions - return nodePolicies.some((policy) => permissions.includes(policy)); + // Node write is allowed if any policy allows it + return policies.some((policy) => policy === 'write'); + } } diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js index 4239f844fce..29beaa6aa12 100644 --- a/ui/app/components/job-page/abstract.js +++ b/ui/app/components/job-page/abstract.js @@ -5,7 +5,6 @@ import classic from 'ember-classic-decorator'; @classic export default class Abstract extends Component { - @service can; @service system; job = null; @@ -21,10 +20,6 @@ export default class Abstract extends Component { // Set to a { title, description } to surface an error errorMessage = null; - get shouldDisplayClientInformation() { - return this.can.can('read client') && this.job.hasClientStatus; - } - @action clearErrorMessage() { this.set('errorMessage', null); diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js index 3f941067c06..daf0e418340 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/parts/job-client-status-summary.js b/ui/app/components/job-page/parts/job-client-status-summary.js index 7e923dacff8..6625e6705a0 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 @@ -2,26 +2,20 @@ import Component from '@ember/component'; import { action, computed } from '@ember/object'; import { classNames } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; -import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic @classNames('boxed-section') export default class JobClientStatusSummary extends Component { job = null; - nodes = null; - forceCollapsed = false; + jobClientStatus = null; gotoClients() {} - @computed('forceCollapsed') + @computed get isExpanded() { - if (this.forceCollapsed) return false; - const storageValue = window.localStorage.nomadExpandJobClientStatusSummary; return storageValue != null ? JSON.parse(storageValue) : true; } - @jobClientStatus('nodes', 'job') jobClientStatus; - @action onSliceClick(ev, slice) { /* eslint-disable-next-line ember/no-string-prototype-extensions */ diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js index dfe42225dc3..d581d88dc29 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/components/job-page/sysbatch.js b/ui/app/components/job-page/sysbatch.js index abdbdfdf3df..0819ed49426 100644 --- a/ui/app/components/job-page/sysbatch.js +++ b/ui/app/components/job-page/sysbatch.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 Sysbatch extends AbstractJobPage {} +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 bf2c0444246..5909c8f163d 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/components/list-accordion/accordion-head.js b/ui/app/components/list-accordion/accordion-head.js index 6ef4f9f2a67..320092634b1 100644 --- a/ui/app/components/list-accordion/accordion-head.js +++ b/ui/app/components/list-accordion/accordion-head.js @@ -9,10 +9,8 @@ export default class AccordionHead extends Component { 'data-test-accordion-head' = true; buttonLabel = 'toggle'; - tooltip = ''; isOpen = false; isExpandable = true; - isDisabled = false; item = null; onClose() {} diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 9feccefa5b7..5779356144c 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -1,5 +1,5 @@ import { inject as service } from '@ember/service'; -import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { action } from '@ember/object'; @@ -25,15 +25,7 @@ export default class IndexController extends Controller.extend( currentPage = 1; - @computed('model.job') - get job() { - return this.model.job; - } - - @computed('model.nodes.[]') - get nodes() { - return this.model.nodes; - } + @alias('model') job; sortProperty = 'name'; sortDescending = false; diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 45d42727ece..fb3aa844521 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -30,14 +30,6 @@ export default class Allocation extends Model { @fragment('resources') allocatedResources; @attr('number') jobVersion; - // Store basic node information returned by the API to avoid the need for - // node:read ACL permission. - @attr('string') nodeName; - @computed - get shortNodeId() { - return this.belongsTo('node').id().split('-')[0]; - } - @attr('number') modifyIndex; @attr('date') modifyTime; diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index 8accf87345d..ba2e81d1cff 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -1,5 +1,4 @@ import Route from '@ember/routing/route'; -import { inject as service } from '@ember/service'; import { collect } from '@ember/object/computed'; import { watchRecord, @@ -10,55 +9,35 @@ import { import WithWatchers from 'nomad-ui/mixins/with-watchers'; export default class IndexRoute extends Route.extend(WithWatchers) { - @service can; - @service store; - async model() { - const job = this.modelFor('jobs.job'); - if (!job) { - return { job, nodes: [] }; - } - - // Optimizing future node look ups by preemptively loading all nodes if - // necessary and allowed. - if (this.can.can('read client') && job.get('hasClientStatus')) { - await this.store.findAll('node'); - } - const nodes = this.store.peekAll('node'); - return { job, nodes }; + // Optimizing future node look ups by preemptively loading everything + await this.store.findAll('node'); + return this.modelFor('jobs.job'); } startWatchers(controller, model) { - if (!model.job) { + if (!model) { return; } controller.set('watchers', { - model: this.watch.perform(model.job), - summary: this.watchSummary.perform(model.job.get('summary')), - allocations: this.watchAllocations.perform(model.job), - evaluations: this.watchEvaluations.perform(model.job), + model: this.watch.perform(model), + summary: this.watchSummary.perform(model.get('summary')), + allocations: this.watchAllocations.perform(model), + evaluations: this.watchEvaluations.perform(model), latestDeployment: - model.job.get('supportsDeployments') && - this.watchLatestDeployment.perform(model.job), + model.get('supportsDeployments') && + this.watchLatestDeployment.perform(model), list: - model.job.get('hasChildren') && - this.watchAllJobs.perform({ - namespace: model.job.namespace.get('name'), - }), - nodes: - this.can.can('read client') && - model.job.get('hasClientStatus') && - this.watchNodes.perform(), + model.get('hasChildren') && + this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), + nodes: model.get('hasClientStatus') && this.watchNodes.perform(), }); } setupController(controller, model) { // Parameterized and periodic detail pages, which list children jobs, // should sort by submit time. - if ( - model.job && - ['periodic', 'parameterized'].includes(model.job.templateType) - ) { + if (model && ['periodic', 'parameterized'].includes(model.templateType)) { controller.setProperties({ sortProperty: 'submitTime', sortDescending: true, diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index 934d69336d6..efa1915524c 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -1,10 +1,8 @@ - {{#if (can "read client")}} - {{#if this.allocation.unhealthyDrivers.length}} - - {{x-icon "alert-triangle" class="is-warning"}} - - {{/if}} + {{#if this.allocation.unhealthyDrivers.length}} + + {{x-icon "alert-triangle" class="is-warning"}} + {{/if}} {{#if this.allocation.nextAllocation}} @@ -40,28 +38,20 @@ {{#if (eq this.context "volume")}} - - {{#if (can "read client")}} - - {{this.allocation.shortNodeId}} - - {{else}} - {{this.allocation.shortNodeId}} - {{/if}} + + + {{this.allocation.node.shortId}} + {{/if}} {{#if (or (eq this.context "taskGroup") (eq this.context "job"))}} {{this.allocation.jobVersion}} - - {{#if (can "read client")}} - - {{this.allocation.shortNodeId}} - - {{else}} - {{this.allocation.shortNodeId}} - {{/if}} + + + {{this.allocation.node.shortId}} + {{else if (or (eq this.context "node") (eq this.context "volume"))}} diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index e42afa68ab3..83c6f1b0f97 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -16,13 +16,13 @@ {{#if this.job.hasClientStatus}} + @jobClientStatus={{this.jobClientStatus}} + /> {{/if}} - + 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 64fbff2f517..4f30af7625e 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 @@ -1,23 +1,17 @@ - +
Job Status in Client - {{#if this.jobClientStatus}} - - {{this.jobClientStatus.totalNodes}} - - {{/if}} + + {{this.jobClientStatus.totalNodes}} +
- {{#if (and this.jobClientStatus (not a.isOpen))}} + {{#unless a.isOpen}}
- {{/if}} + {{/unless}}
- {{#if this.jobClientStatus}} - -
    - {{#each chart.data as |datum index|}} -
  1. +
      + {{#each chart.data as |datum index|}} +
    1. - {{#if (gt datum.value 0)}} - - - - {{else}} - - {{/if}} -
    2. - {{/each}} -
    - - {{/if}} + {{if (eq datum.value 0) "is-empty" "is-clickable"}}" + > + {{#if (gt datum.value 0)}} + + + + {{else}} + + {{/if}} +
  2. + {{/each}} +
+
\ 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 61ec3e17974..65bcad3cc93 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -16,13 +16,13 @@ {{#if this.job.hasClientStatus}} + @jobClientStatus={{this.jobClientStatus}} + /> {{/if}} - + diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs index 429002bf0fb..c5fd40faaa3 100644 --- a/ui/app/templates/components/job-page/sysbatch.hbs +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -7,11 +7,10 @@ - + diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 16a85e4405b..76b1ad7c225 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -13,11 +13,10 @@ - + diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index eb33c641961..67d9974dbc5 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -8,7 +8,7 @@ {{/if}}
  • Allocations
  • Evaluations
  • - {{#if (and (can "read client") (and this.job.hasClientStatus (not this.job.hasChildren)))}} + {{#if (and this.job.hasClientStatus (not this.job.hasChildren))}}
  • Clients
  • {{/if}} diff --git a/ui/app/templates/components/list-accordion/accordion-head.hbs b/ui/app/templates/components/list-accordion/accordion-head.hbs index 52e9931ddf3..16359b72402 100644 --- a/ui/app/templates/components/list-accordion/accordion-head.hbs +++ b/ui/app/templates/components/list-accordion/accordion-head.hbs @@ -3,9 +3,7 @@ - - - - <:after-namespace> - - Cron - {{this.job.periodicDetails.Spec}} + + + + + + periodic - - - - - - - - + + + + <:after-namespace> + + + Cron + + {{@job.periodicDetails.Spec}} + + + + + + + + \ No newline at end of file From c3b88112e9e0048d2c15cd28d3dc43d63c52364e Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 09:53:59 -0500 Subject: [PATCH 08/37] refact: job-page/periodic-child template to use contextual components --- .../components/job-page/periodic-child.hbs | 70 +++++++++---------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index 65bcad3cc93..3a76c123da5 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -1,38 +1,34 @@ - - - - - - - <:before-namespace> - - Parent - - {{this.job.parent.name}} - - - - - - {{#if this.job.hasClientStatus}} - + + + + + <:before-namespace> + + + Parent + + + {{this.job.parent.name}} + + + + + {{#if @job.hasClientStatus}} + + {{/if}} + + + - {{/if}} - - - - - - - - - - - + + + + \ No newline at end of file From e40fab4edbec229bde7da41bdb23fc7599b9b060 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 09:54:21 -0500 Subject: [PATCH 09/37] refact: job-page/parameterized template to use contextual components --- .../components/job-page/parameterized.hbs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/ui/app/templates/components/job-page/parameterized.hbs b/ui/app/templates/components/job-page/parameterized.hbs index ea77a4d7297..30368532a9c 100644 --- a/ui/app/templates/components/job-page/parameterized.hbs +++ b/ui/app/templates/components/job-page/parameterized.hbs @@ -1,19 +1,19 @@ - - - - - Parameterized - - - - - - - - - + + + + + + Parameterized + + + + + + + + \ No newline at end of file From 4076e26ad7049920e13b936772483603d88ea7cd Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 09:54:37 -0500 Subject: [PATCH 10/37] refact: job-page/parameterized-child template to use contextual components --- .../job-page/parameterized-child.hbs | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 83c6f1b0f97..a542acedae1 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -1,63 +1,71 @@ - - - - - - - <:before-namespace> - - Parent - - {{this.job.parent.name}} - - - - - - {{#if this.job.hasClientStatus}} - + + + + + <:before-namespace> + + + Parent + + + {{this.job.parent.name}} + + + + + {{#if this.job.hasClientStatus}} + + {{/if}} + + + - {{/if}} - - - - - - - - - -
    -
    - Meta -
    - {{#if this.job.definition.Meta}} - - {{else}} -
    -
    -

    No Meta Attributes

    -

    This job is configured with no meta attributes.

    -
    + +
    +
    + Meta
    - {{/if}} -
    - -
    -
    Payload
    -
    - {{#if this.payloadJSON}} - + {{#if @job.definition.Meta}} + {{else}} -
    {{this.payload}}
    +
    +
    +

    + No Meta Attributes +

    +

    + This job is configured with no meta attributes. +

    +
    +
    {{/if}}
    -
    - \ No newline at end of file +
    +
    + Payload +
    +
    + {{#if this.payloadJSON}} + + {{else}} +
    +            
    +              {{this.payload}}
    +            
    +          
    + {{/if}} +
    +
    + + \ No newline at end of file From 4aa035b6faa2812219e5d439b9a234003a86b3a3 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 09:54:54 -0500 Subject: [PATCH 11/37] refact: job-page/batch template to use contextual components --- .../templates/components/job-page/batch.hbs | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/ui/app/templates/components/job-page/batch.hbs b/ui/app/templates/components/job-page/batch.hbs index 6030a60208c..8e8a6928054 100644 --- a/ui/app/templates/components/job-page/batch.hbs +++ b/ui/app/templates/components/job-page/batch.hbs @@ -1,21 +1,16 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file From dec6a68ca4d13ec5e2ab98ea9e04bf4b3749a88d Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 09:57:39 -0500 Subject: [PATCH 12/37] refact: remove unused backing component classes and extend periodic and param-child to use Glimmer --- ui/app/components/job-page/abstract.js | 32 ----------------- ui/app/components/job-page/batch.js | 5 --- .../job-page/parameterized-child.js | 15 ++------ ui/app/components/job-page/parameterized.js | 5 --- ui/app/components/job-page/periodic-child.js | 34 ------------------- ui/app/components/job-page/periodic.js | 26 +++----------- ui/app/components/job-page/service.js | 5 --- ui/app/components/job-page/sysbatch.js | 15 -------- ui/app/components/job-page/system.js | 15 -------- 9 files changed, 7 insertions(+), 145 deletions(-) delete mode 100644 ui/app/components/job-page/abstract.js delete mode 100644 ui/app/components/job-page/batch.js delete mode 100644 ui/app/components/job-page/parameterized.js delete mode 100644 ui/app/components/job-page/periodic-child.js delete mode 100644 ui/app/components/job-page/service.js delete mode 100644 ui/app/components/job-page/sysbatch.js delete mode 100644 ui/app/components/job-page/system.js diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js deleted file mode 100644 index 29beaa6aa12..00000000000 --- a/ui/app/components/job-page/abstract.js +++ /dev/null @@ -1,32 +0,0 @@ -import Component from '@ember/component'; -import { inject as service } from '@ember/service'; -import { action } from '@ember/object'; -import classic from 'ember-classic-decorator'; - -@classic -export default class Abstract extends Component { - @service system; - - job = null; - - // Provide a value that is bound to a query param - sortProperty = null; - sortDescending = null; - - // Provide actions that require routing - gotoTaskGroup() {} - gotoJob() {} - - // Set to a { title, description } to surface an error - errorMessage = null; - - @action - clearErrorMessage() { - this.set('errorMessage', null); - } - - @action - handleError(errorObject) { - this.set('errorMessage', errorObject); - } -} diff --git a/ui/app/components/job-page/batch.js b/ui/app/components/job-page/batch.js deleted file mode 100644 index 3aa31786c3a..00000000000 --- a/ui/app/components/job-page/batch.js +++ /dev/null @@ -1,5 +0,0 @@ -import AbstractJobPage from './abstract'; -import classic from 'ember-classic-decorator'; - -@classic -export default class Batch extends AbstractJobPage {} diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js index daf0e418340..70eb063bc4a 100644 --- a/ui/app/components/job-page/parameterized-child.js +++ b/ui/app/components/job-page/parameterized-child.js @@ -1,14 +1,9 @@ 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'; +import Component from '@glimmer/component'; -@classic -export default class ParameterizedChild extends PeriodicChildJobPage { +export default class ParameterizedChild extends Component { @alias('job.decodedPayload') payload; - @service store; @computed('payload') get payloadJSON() { @@ -20,10 +15,4 @@ 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/parameterized.js b/ui/app/components/job-page/parameterized.js deleted file mode 100644 index cf69268ae06..00000000000 --- a/ui/app/components/job-page/parameterized.js +++ /dev/null @@ -1,5 +0,0 @@ -import AbstractJobPage from './abstract'; -import classic from 'ember-classic-decorator'; - -@classic -export default class Parameterized extends AbstractJobPage {} diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js deleted file mode 100644 index d581d88dc29..00000000000 --- a/ui/app/components/job-page/periodic-child.js +++ /dev/null @@ -1,34 +0,0 @@ -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; - const parent = this.get('job.parent'); - - return [ - { label: 'Jobs', args: ['jobs'] }, - { - label: parent.get('name'), - args: ['jobs.job', parent], - }, - { - label: job.get('trimmedName'), - args: ['jobs.job', job], - }, - ]; - } - - @jobClientStatus('nodes', 'job') jobClientStatus; - - get nodes() { - return this.store.peekAll('node'); - } -} diff --git a/ui/app/components/job-page/periodic.js b/ui/app/components/job-page/periodic.js index 8a3783bb722..d744836f1fd 100644 --- a/ui/app/components/job-page/periodic.js +++ b/ui/app/components/job-page/periodic.js @@ -1,27 +1,11 @@ -import AbstractJobPage from './abstract'; -import { inject as service } from '@ember/service'; import { action } from '@ember/object'; -import classic from 'ember-classic-decorator'; -import messageForError from 'nomad-ui/utils/message-from-adapter-error'; - -@classic -export default class Periodic extends AbstractJobPage { - @service store; - - errorMessage = null; +import Component from '@glimmer/component'; +export default class Periodic extends Component { @action - forceLaunch() { - this.job.forcePeriodic().catch((err) => { - this.set('errorMessage', { - title: 'Could Not Force Launch', - description: messageForError(err, 'submit jobs'), - }); + forceLaunch(setError) { + this.args.job.forcePeriodic().catch((err) => { + setError(err); }); } - - @action - clearErrorMessage() { - this.set('errorMessage', null); - } } diff --git a/ui/app/components/job-page/service.js b/ui/app/components/job-page/service.js deleted file mode 100644 index 0cb58e90c3b..00000000000 --- a/ui/app/components/job-page/service.js +++ /dev/null @@ -1,5 +0,0 @@ -import AbstractJobPage from './abstract'; -import classic from 'ember-classic-decorator'; - -@classic -export default class Service extends AbstractJobPage {} diff --git a/ui/app/components/job-page/sysbatch.js b/ui/app/components/job-page/sysbatch.js deleted file mode 100644 index 0819ed49426..00000000000 --- a/ui/app/components/job-page/sysbatch.js +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 5909c8f163d..00000000000 --- a/ui/app/components/job-page/system.js +++ /dev/null @@ -1,15 +0,0 @@ -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 { - @service store; - - @jobClientStatus('nodes', 'job') jobClientStatus; - - get nodes() { - return this.store.peekAll('node'); - } -} From 204cbea29dc576b348ecc24215066291687b204e Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 10:53:52 -0500 Subject: [PATCH 13/37] refact: move conditional rendering logic from job-page to job-client-status-summary --- .../job-page/parameterized-child.hbs | 10 +- .../parts/job-client-status-summary.hbs | 138 +++++++++--------- .../components/job-page/periodic-child.hbs | 4 +- 3 files changed, 75 insertions(+), 77 deletions(-) diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index a542acedae1..30e4beb7715 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -18,12 +18,10 @@ - {{#if this.job.hasClientStatus}} - - {{/if}} + - -
    -
    - Job Status in Client - - {{this.jobClientStatus.totalNodes}} - - - {{x-icon "info-circle-outline" class="is-faded"}} - -
    - {{#unless a.isOpen}} -
    -
    - -
    +{{#if @job.hasClientStatus}} + + +
    +
    + Job Status in Client + + {{this.jobClientStatus.totalNodes}} + + + {{x-icon "info-circle-outline" class="is-faded"}} +
    - {{/unless}} -
    -
    - - -
      - {{#each chart.data as |datum index|}} -
    1. +
      + +
      +
    + {{/unless}} +
    +
    + + +
      + {{#each chart.data as |datum index|}} +
    1. - {{#if (gt datum.value 0)}} - + {{if (eq datum.value 0) "is-empty" "is-clickable"}}" + > + {{#if (gt datum.value 0)}} + + + + {{else}} - - {{else}} - - {{/if}} -
    2. - {{/each}} -
    -
    -
    - \ No newline at end of file + {{/if}} + + {{/each}} + + + + +{{/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 3a76c123da5..f16c3d3dca3 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -18,9 +18,7 @@ - {{#if @job.hasClientStatus}} - - {{/if}} + Date: Mon, 3 Jan 2022 11:13:18 -0500 Subject: [PATCH 14/37] refactor: compute jobClientStatus in summary backing class component --- .../job-page/parts/job-client-status-summary.js | 11 ++++++++++- .../components/job-page/parameterized-child.hbs | 5 +---- .../job-page/parts/job-client-status-summary.hbs | 3 ++- 3 files changed, 13 insertions(+), 6 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 6625e6705a0..e583aae2496 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,13 +1,22 @@ 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'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic @classNames('boxed-section') export default class JobClientStatusSummary extends Component { + @service store; + + @jobClientStatus('nodes', 'job') jobClientStatus; + + get nodes() { + return this.store.peekAll('node'); + } + job = null; - jobClientStatus = null; gotoClients() {} @computed diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 30e4beb7715..1029bbab764 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -18,10 +18,7 @@ - + + {{debugger}} Date: Mon, 3 Jan 2022 11:33:47 -0500 Subject: [PATCH 15/37] chore: prettify task-groups template --- .../components/job-page/parts/task-groups.hbs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/ui/app/templates/components/job-page/parts/task-groups.hbs b/ui/app/templates/components/job-page/parts/task-groups.hbs index aaeb0f95ebb..ab228b57caa 100644 --- a/ui/app/templates/components/job-page/parts/task-groups.hbs +++ b/ui/app/templates/components/job-page/parts/task-groups.hbs @@ -6,21 +6,38 @@ + @sortDescending={{this.sortDescending}} as |t| + > - Name - Count - Allocation Status - Volume - Reserved CPU - Reserved Memory - Reserved Disk + + Name + + + Count + + + Allocation Status + + + Volume + + + Reserved CPU + + + Reserved Memory + + + Reserved Disk + - + @onClick={{action this.gotoTaskGroup row.model}} + />
    -
    + \ No newline at end of file From 432758916e7e323797ec07f5748a61add827501e Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 11:35:41 -0500 Subject: [PATCH 16/37] refact: move gotoTaskGroup action to component Previously, the router service was not available to components. Now that it is, we no longer need to prop-drill this linking action. --- ui/app/components/job-page/parts/task-groups.js | 11 ++++++++--- ui/app/controllers/jobs/job/index.js | 9 --------- ui/app/templates/components/job-page/batch.hbs | 1 - .../components/job-page/parameterized-child.hbs | 6 +----- .../components/job-page/parts/task-groups.hbs | 2 +- .../templates/components/job-page/periodic-child.hbs | 6 +----- ui/app/templates/components/job-page/service.hbs | 6 +----- ui/app/templates/components/job-page/sysbatch.hbs | 6 +----- ui/app/templates/components/job-page/system.hbs | 6 +----- ui/app/templates/jobs/job/index.hbs | 4 ++-- 10 files changed, 16 insertions(+), 41 deletions(-) diff --git a/ui/app/components/job-page/parts/task-groups.js b/ui/app/components/job-page/parts/task-groups.js index 129bb5f5aee..892e78e2191 100644 --- a/ui/app/components/job-page/parts/task-groups.js +++ b/ui/app/components/job-page/parts/task-groups.js @@ -1,5 +1,6 @@ import Component from '@ember/component'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; +import { inject as service } from '@ember/service'; import { alias } from '@ember/object/computed'; import Sortable from 'nomad-ui/mixins/sortable'; import { classNames } from '@ember-decorators/component'; @@ -8,14 +9,18 @@ import classic from 'ember-classic-decorator'; @classic @classNames('boxed-section') export default class TaskGroups extends Component.extend(Sortable) { + @service router; + job = null; // Provide a value that is bound to a query param sortProperty = null; sortDescending = null; - // Provide an action with access to the router - gotoTaskGroup() {} + @action + gotoTaskGroup(taskGroup) { + this.router.transitionTo('jobs.job.task-group', this.job, taskGroup); + } @computed('job.taskGroups.[]') get taskGroups() { diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 5779356144c..7f4341dd601 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -30,15 +30,6 @@ export default class IndexController extends Controller.extend( sortProperty = 'name'; sortDescending = false; - @action - gotoTaskGroup(taskGroup) { - this.transitionToRoute( - 'jobs.job.task-group', - taskGroup.get('job'), - taskGroup - ); - } - @action gotoJob(job) { this.transitionToRoute('jobs.job', job, { diff --git a/ui/app/templates/components/job-page/batch.hbs b/ui/app/templates/components/job-page/batch.hbs index 8e8a6928054..b015faa51fc 100644 --- a/ui/app/templates/components/job-page/batch.hbs +++ b/ui/app/templates/components/job-page/batch.hbs @@ -8,7 +8,6 @@ diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 1029bbab764..cb98f1bcdac 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -21,11 +21,7 @@ - +
    diff --git a/ui/app/templates/components/job-page/parts/task-groups.hbs b/ui/app/templates/components/job-page/parts/task-groups.hbs index ab228b57caa..9b55029c726 100644 --- a/ui/app/templates/components/job-page/parts/task-groups.hbs +++ b/ui/app/templates/components/job-page/parts/task-groups.hbs @@ -35,7 +35,7 @@ diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index f16c3d3dca3..0a57c3bb5ef 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -21,11 +21,7 @@ - + diff --git a/ui/app/templates/components/job-page/service.hbs b/ui/app/templates/components/job-page/service.hbs index 7f9317d5212..233e85d3f30 100644 --- a/ui/app/templates/components/job-page/service.hbs +++ b/ui/app/templates/components/job-page/service.hbs @@ -7,11 +7,7 @@ - + diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs index 514a5d17184..863f4351fa4 100644 --- a/ui/app/templates/components/job-page/sysbatch.hbs +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -6,11 +6,7 @@ - + diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 987acbae5df..1c9e06371c4 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -7,11 +7,7 @@ - + diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index 41834690f89..209d22a3f90 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -1,10 +1,10 @@ {{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") gotoClients=(action "gotoClients") }} \ No newline at end of file From da2f574c455e97fc28d331268622bd8b91042df4 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 11:37:57 -0500 Subject: [PATCH 17/37] chore: prettify job-row template --- ui/app/templates/components/job-row.hbs | 37 +++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index 6475ab620db..eeeb523e424 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -1,15 +1,34 @@ -{{this.job.name}} + + + {{this.job.name}} + + {{#if this.system.shouldShowNamespaces}} - {{this.job.namespace.name}} + + {{this.job.namespace.name}} + {{/if}} {{#if (eq @context "child")}} - {{format-month-ts this.job.submitTime}} + + {{format-month-ts this.job.submitTime}} + {{/if}} - {{this.job.status}} + + {{this.job.status}} + + + + {{this.job.displayType}} + + + {{this.job.priority}} -{{this.job.displayType}} -{{this.job.priority}} {{#if this.job.taskGroupCount}} {{this.job.taskGroupCount}} @@ -23,10 +42,12 @@ {{#if (gt this.job.totalChildren 0)}} {{else}} - No Children + + No Children + {{/if}} {{else}} {{/if}}
    - + \ No newline at end of file From 6bbcc101823dd4fab4992c55d62fd817996e0142 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 11:44:30 -0500 Subject: [PATCH 18/37] chore: prettify jobs index template --- ui/app/templates/jobs/job/index.hbs | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index 209d22a3f90..f893b4e29a3 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -5,6 +5,5 @@ sortProperty=this.sortProperty sortDescending=this.sortDescending currentPage=this.currentPage - gotoJob=(action "gotoJob") gotoClients=(action "gotoClients") }} \ No newline at end of file From 2979538d13ddcc4d7e89897caeaddd1e9cd70ea7 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 12:04:44 -0500 Subject: [PATCH 19/37] chore: prettify children template --- .../components/job-page/parts/children.hbs | 75 ++++++++++++++----- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/ui/app/templates/components/job-page/parts/children.hbs b/ui/app/templates/components/job-page/parts/children.hbs index 4070bfa06b3..dc6c27517b8 100644 --- a/ui/app/templates/components/job-page/parts/children.hbs +++ b/ui/app/templates/components/job-page/parts/children.hbs @@ -5,7 +5,8 @@ + class="button is-primary is-compact pull-right" + > Dispatch Job {{else}} @@ -14,7 +15,8 @@ class="button is-disabled is-primary is-compact pull-right tooltip multiline" aria-label="You don’t have permission to dispatch jobs" disabled - type="button"> + type="button" + > Dispatch Job {{/if}} @@ -25,44 +27,79 @@ + @page={{this.currentPage}} as |p| + > + @class="with-foot" as |t| + > - Name + + Name + {{#if this.system.shouldShowNamespaces}} - Namespace + + Namespace + {{/if}} - Submitted At - Status - Type - Priority - Groups - Summary + + Submitted At + + + Status + + + Type + + + Priority + + + Groups + + + Summary + - +
    {{else}}
    -

    No Job Launches

    -

    No remaining living job launches.

    +

    + No Job Launches +

    +

    + No remaining living job launches. +

    {{/if}} -
    + \ No newline at end of file From 6b26fe80df5175fda4f2c84892b9fec1539c24b7 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 12:05:20 -0500 Subject: [PATCH 20/37] chore: prettify job/index controller --- ui/app/controllers/jobs/job/index.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 7f4341dd601..644f1b24723 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -30,13 +30,6 @@ export default class IndexController extends Controller.extend( sortProperty = 'name'; sortDescending = false; - @action - gotoJob(job) { - this.transitionToRoute('jobs.job', job, { - queryParams: { jobNamespace: job.get('namespace.name') }, - }); - } - @action gotoClients(statusFilter) { this.transitionToRoute('jobs.job.clients', this.job, { From 9f1d22595cd605adc7e4a2b9a27e315ebfa4f3db Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 12:08:07 -0500 Subject: [PATCH 21/37] refact: move gotoJob to component --- ui/app/components/job-page/parts/children.js | 3 - ui/app/components/job-row.js | 18 +- ui/app/controllers/jobs/index.js | 7 - .../components/job-page/parameterized.hbs | 1 - .../components/job-page/parts/children.hbs | 7 +- .../components/job-page/periodic.hbs | 1 - ui/app/templates/jobs/index.hbs | 202 ++++++++++++------ 7 files changed, 145 insertions(+), 94 deletions(-) diff --git a/ui/app/components/job-page/parts/children.js b/ui/app/components/job-page/parts/children.js index 20f72910dc9..37aed7baea3 100644 --- a/ui/app/components/job-page/parts/children.js +++ b/ui/app/components/job-page/parts/children.js @@ -19,9 +19,6 @@ export default class Children extends Component.extend(Sortable) { sortDescending = null; currentPage = null; - // Provide an action with access to the router - gotoJob() {} - @readOnly('userSettings.pageSize') pageSize; @computed('job.taskGroups.[]') diff --git a/ui/app/components/job-row.js b/ui/app/components/job-row.js index 7413827b054..5d5dbd056f5 100644 --- a/ui/app/components/job-row.js +++ b/ui/app/components/job-row.js @@ -1,5 +1,6 @@ -import { inject as service } from '@ember/service'; import Component from '@ember/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; import { lazyClick } from '../helpers/lazy-click'; import { classNames, tagName } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; @@ -8,8 +9,9 @@ import classic from 'ember-classic-decorator'; @tagName('tr') @classNames('job-row', 'is-interactive') export default class JobRow extends Component { - @service system; + @service router; @service store; + @service system; job = null; @@ -17,9 +19,15 @@ export default class JobRow extends Component { // based on the relationship of this job to others. context = 'independent'; - onClick() {} - click(event) { - lazyClick([this.onClick, event]); + lazyClick([this.gotoJob, event]); + } + + @action + gotoJob() { + const { job } = this; + this.router.transitionTo('jobs.job', job, { + queryParams: { namespace: job.get('namespace.name') }, + }); } } diff --git a/ui/app/controllers/jobs/index.js b/ui/app/controllers/jobs/index.js index ff38e023b9b..f47792c03f3 100644 --- a/ui/app/controllers/jobs/index.js +++ b/ui/app/controllers/jobs/index.js @@ -260,11 +260,4 @@ export default class IndexController extends Controller.extend( setFacetQueryParam(queryParam, selection) { this.set(queryParam, serialize(selection)); } - - @action - gotoJob(job) { - this.transitionToRoute('jobs.job', job.get('plainId'), { - queryParams: { namespace: job.get('namespace.name') }, - }); - } } diff --git a/ui/app/templates/components/job-page/parameterized.hbs b/ui/app/templates/components/job-page/parameterized.hbs index 30368532a9c..0277a089e0d 100644 --- a/ui/app/templates/components/job-page/parameterized.hbs +++ b/ui/app/templates/components/job-page/parameterized.hbs @@ -12,7 +12,6 @@ @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} @currentPage={{@currentPage}} - @gotoJob={{@gotoJob}} />
    diff --git a/ui/app/templates/components/job-page/parts/children.hbs b/ui/app/templates/components/job-page/parts/children.hbs index dc6c27517b8..89da35b6a4f 100644 --- a/ui/app/templates/components/job-page/parts/children.hbs +++ b/ui/app/templates/components/job-page/parts/children.hbs @@ -64,12 +64,7 @@ - +
    diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs index a94ded0399e..dfef27fc337 100644 --- a/ui/app/templates/components/job-page/periodic.hbs +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -29,7 +29,6 @@ @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} @currentPage={{@currentPage}} - @gotoJob={{@gotoJob}} /> diff --git a/ui/app/templates/jobs/index.hbs b/ui/app/templates/jobs/index.hbs index 3962ac33256..903453591f9 100644 --- a/ui/app/templates/jobs/index.hbs +++ b/ui/app/templates/jobs/index.hbs @@ -7,13 +7,21 @@ data-test-jobs-search @searchTerm={{mut this.searchTerm}} @onChange={{action this.resetPagination}} - @placeholder="Search jobs..." /> + @placeholder="Search jobs..." + /> {{/if}}
    {{#if (media "isMobile")}}
    {{#if (can "run job" namespace=this.qpNamespace)}} - Run Job + + Run Job + {{else}} + > + Run Job + {{/if}}
    {{/if}} @@ -33,41 +43,52 @@ @label="Namespace" @options={{this.optionsNamespaces}} @selection={{this.qpNamespace}} - @onSelect={{action (queue - (action this.cacheNamespace) - (action this.setFacetQueryParam "qpNamespace") - )}} /> + @onSelect={{action + (queue (action this.cacheNamespace) (action this.setFacetQueryParam "qpNamespace")) + }} + /> {{/if}} + @onSelect={{action this.setFacetQueryParam "qpType"}} + /> + @onSelect={{action this.setFacetQueryParam "qpStatus"}} + /> + @onSelect={{action this.setFacetQueryParam "qpDatacenter"}} + /> + @onSelect={{action this.setFacetQueryParam "qpPrefix"}} + /> {{#if (not (media "isMobile"))}}
    {{#if (can "run job" namespace=this.qpNamespace)}} - Run Job + + Run Job + {{else}} + > + Run Job + {{/if}}
    {{/if}} {{#if this.isForbidden}} - {{else}} - {{#if this.sortedJobs}} - - - - Name - {{#if this.system.shouldShowNamespaces}} - Namespace + {{else if this.sortedJobs}} + + + + + Name + + {{#if this.system.shouldShowNamespaces}} + + Namespace + + {{/if}} + + Status + + + Type + + + Priority + + + Groups + + + Summary + + + + + + +
    + +
    - {{/if}} +
    + {{else}} +
    + {{#if (eq this.visibleJobs.length 0)}} +

    + No Jobs +

    +

    + The cluster is currently empty. +

    + {{else if (eq this.filteredJobs.length 0)}} +

    + No Matches +

    +

    + No jobs match your current filter selection. +

    + {{else if this.searchTerm}} +

    + No Matches +

    +

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

    + {{/if}} +
    {{/if}} - + \ No newline at end of file From d64df8ba6813a8b7a422da571adf277e37bde16b Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 12:34:02 -0500 Subject: [PATCH 22/37] fix: router expect identifier instead of model for trans to job page --- ui/app/components/job-row.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/job-row.js b/ui/app/components/job-row.js index 5d5dbd056f5..c353813c0bd 100644 --- a/ui/app/components/job-row.js +++ b/ui/app/components/job-row.js @@ -26,7 +26,7 @@ export default class JobRow extends Component { @action gotoJob() { const { job } = this; - this.router.transitionTo('jobs.job', job, { + this.router.transitionTo('jobs.job', job.plainId, { queryParams: { namespace: job.get('namespace.name') }, }); } From 2778fa1ffaafea3bc9a408a93683dac85609c01f Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Mon, 3 Jan 2022 12:34:16 -0500 Subject: [PATCH 23/37] fix: remove debugger --- .../components/job-page/parts/job-client-status-summary.hbs | 1 - 1 file changed, 1 deletion(-) 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 9d79deae0a8..e7831f45341 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 @@ -35,7 +35,6 @@ - {{debugger}} Date: Mon, 3 Jan 2022 12:35:00 -0500 Subject: [PATCH 24/37] refact: move gotoClients logic down to component --- .../job-page/parts/job-client-status-summary.js | 12 +++++++++++- ui/app/controllers/jobs/job/index.js | 15 ++------------- .../components/job-page/parameterized-child.hbs | 2 +- .../components/job-page/periodic-child.hbs | 2 +- ui/app/templates/components/job-page/sysbatch.hbs | 2 +- ui/app/templates/components/job-page/system.hbs | 2 +- ui/app/templates/jobs/job/index.hbs | 1 - 7 files changed, 17 insertions(+), 19 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 e583aae2496..6aa683ce39c 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 @@ -8,6 +8,7 @@ import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic @classNames('boxed-section') export default class JobClientStatusSummary extends Component { + @service router; @service store; @jobClientStatus('nodes', 'job') jobClientStatus; @@ -17,7 +18,16 @@ export default class JobClientStatusSummary extends Component { } job = null; - gotoClients() {} + + @action + gotoClients(statusFilter) { + this.router.transitionTo('jobs.job.clients', this.job, { + queryParams: { + status: JSON.stringify(statusFilter), + namespace: this.job.get('namespace.name'), + }, + }); + } @computed get isExpanded() { diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 644f1b24723..f066ac554b2 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -1,8 +1,7 @@ -import { inject as service } from '@ember/service'; -import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; -import { action } from '@ember/object'; import classic from 'ember-classic-decorator'; @classic @@ -29,14 +28,4 @@ export default class IndexController extends Controller.extend( sortProperty = 'name'; sortDescending = false; - - @action - gotoClients(statusFilter) { - this.transitionToRoute('jobs.job.clients', this.job, { - queryParams: { - status: JSON.stringify(statusFilter), - namespace: this.job.get('namespace.name'), - }, - }); - } } diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index cb98f1bcdac..7acbeecee24 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -18,7 +18,7 @@
    - + diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index 0a57c3bb5ef..89f20b7f1ea 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -18,7 +18,7 @@ - + diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs index 863f4351fa4..2b9c7e0c0f8 100644 --- a/ui/app/templates/components/job-page/sysbatch.hbs +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -3,7 +3,7 @@ - + diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 1c9e06371c4..693e74bccbd 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -4,7 +4,7 @@ - + diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index f893b4e29a3..0ff6bd3acbb 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -5,5 +5,4 @@ sortProperty=this.sortProperty sortDescending=this.sortDescending currentPage=this.currentPage - gotoClients=(action "gotoClients") }} \ No newline at end of file From 0eff35dde0dbaebbab286e1da2457a934f7e3b77 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 13 Dec 2021 16:56:58 -0500 Subject: [PATCH 25/37] ui: add "read client" ability --- ui/app/abilities/client.js | 33 +++++++++++++++++++------- ui/tests/unit/abilities/client-test.js | 31 ++++++++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/ui/app/abilities/client.js b/ui/app/abilities/client.js index 465f1b4a131..cd1fc8ed1f7 100644 --- a/ui/app/abilities/client.js +++ b/ui/app/abilities/client.js @@ -7,6 +7,9 @@ import classic from 'ember-classic-decorator'; export default class Client extends AbstractAbility { // Map abilities to policy options (which are coarse for nodes) // instead of specific behaviors. + @or('bypassAuthorization', 'selfTokenIsManagement', 'policiesIncludeNodeRead') + canRead; + @or( 'bypassAuthorization', 'selfTokenIsManagement', @@ -15,14 +18,28 @@ export default class Client extends AbstractAbility { canWrite; @computed('token.selfTokenPolicies.[]') - get policiesIncludeNodeWrite() { - // For each policy record, extract the Node policy - const policies = (this.get('token.selfTokenPolicies') || []) - .toArray() - .map((policy) => get(policy, 'rulesJSON.Node.Policy')) - .compact(); + get policiesIncludeNodeRead() { + return policiesIncludePermissions(this.get('token.selfTokenPolicies'), [ + 'read', + 'write', + ]); + } - // Node write is allowed if any policy allows it - return policies.some((policy) => policy === 'write'); + @computed('token.selfTokenPolicies.[]') + get policiesIncludeNodeWrite() { + return policiesIncludePermissions(this.get('token.selfTokenPolicies'), [ + 'write', + ]); } } + +function policiesIncludePermissions(policies = [], permissions = []) { + // For each policy record, extract the Node policy + const nodePolicies = policies + .toArray() + .map((policy) => get(policy, 'rulesJSON.Node.Policy')) + .compact(); + + // Check for requested permissions + return nodePolicies.some((policy) => permissions.includes(policy)); +} diff --git a/ui/tests/unit/abilities/client-test.js b/ui/tests/unit/abilities/client-test.js index 08850b60f2f..8d2c2a73110 100644 --- a/ui/tests/unit/abilities/client-test.js +++ b/ui/tests/unit/abilities/client-test.js @@ -8,26 +8,28 @@ module('Unit | Ability | client', function (hooks) { setupTest(hooks); setupAbility('client')(hooks); - test('it permits client write when ACLs are disabled', function (assert) { + test('it permits client read and write when ACLs are disabled', function (assert) { const mockToken = Service.extend({ aclEnabled: false, }); this.owner.register('service:token', mockToken); + assert.ok(this.ability.canRead); assert.ok(this.ability.canWrite); }); - test('it permits client write for management tokens', function (assert) { + test('it permits client read and write for management tokens', function (assert) { const mockToken = Service.extend({ aclEnabled: true, selfToken: { type: 'management' }, }); this.owner.register('service:token', mockToken); + assert.ok(this.ability.canRead); assert.ok(this.ability.canWrite); }); - test('it permits client write for tokens with a policy that has node-write', function (assert) { + test('it permits client read and write for tokens with a policy that has node-write', function (assert) { const mockToken = Service.extend({ aclEnabled: true, selfToken: { type: 'client' }, @@ -43,10 +45,11 @@ module('Unit | Ability | client', function (hooks) { }); this.owner.register('service:token', mockToken); + assert.ok(this.ability.canRead); assert.ok(this.ability.canWrite); }); - test('it permits client write for tokens with a policy that allows write and another policy that disallows it', function (assert) { + test('it permits client read and write for tokens with a policy that allows write and another policy that disallows it', function (assert) { const mockToken = Service.extend({ aclEnabled: true, selfToken: { type: 'client' }, @@ -69,10 +72,11 @@ module('Unit | Ability | client', function (hooks) { }); this.owner.register('service:token', mockToken); + assert.ok(this.ability.canRead); assert.ok(this.ability.canWrite); }); - test('it blocks client write for tokens with a policy that does not allow node-write', function (assert) { + test('it permits client read and blocks client write for tokens with a policy that does not allow node-write', function (assert) { const mockToken = Service.extend({ aclEnabled: true, selfToken: { type: 'client' }, @@ -88,6 +92,23 @@ module('Unit | Ability | client', function (hooks) { }); this.owner.register('service:token', mockToken); + assert.ok(this.ability.canRead); + assert.notOk(this.ability.canWrite); + }); + + test('it blocks client read and write for tokens without a node policy', function (assert) { + const mockToken = Service.extend({ + aclEnabled: true, + selfToken: { type: 'client' }, + selfTokenPolicies: [ + { + rulesJSON: {}, + }, + ], + }); + this.owner.register('service:token', mockToken); + + assert.notOk(this.ability.canRead); assert.notOk(this.ability.canWrite); }); }); From 1751f5b32b25fa691acca489a8358e19d9c25dac Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:43:51 -0500 Subject: [PATCH 26/37] fix: move node loading to jobs.job route and add auth logic --- ui/app/controllers/jobs/job/clients.js | 4 +--- ui/app/routes/jobs/job.js | 5 +++++ ui/app/routes/jobs/job/index.js | 8 +++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/ui/app/controllers/jobs/job/clients.js b/ui/app/controllers/jobs/job/clients.js index a4b7f05dffd..a7735784c7a 100644 --- a/ui/app/controllers/jobs/job/clients.js +++ b/ui/app/controllers/jobs/job/clients.js @@ -67,9 +67,7 @@ export default class ClientsController extends Controller.extend( @computed('store') get allNodes() { - return this.store.peekAll('node').length - ? this.store.peekAll('node') - : this.store.findAll('node'); + return this.store.peekAll('node'); } @computed('allNodes', 'jobClientStatus.byNode') diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js index 3a01c84ba33..a39c1618315 100644 --- a/ui/app/routes/jobs/job.js +++ b/ui/app/routes/jobs/job.js @@ -33,6 +33,11 @@ export default class JobRoute extends Route { relatedModelsQueries.push(job.get('recommendationSummaries')); } + // Optimizing future node look ups by preemptively loading everything + if (job.get('hasClientStatus') && this.can.can('read client')) { + relatedModelsQueries.push(this.store.findAll('node')); + } + return RSVP.all(relatedModelsQueries).then(() => job); }) .catch(notifyError(this)); diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index ba2e81d1cff..f5b7d7b3205 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -1,3 +1,4 @@ +import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; import { collect } from '@ember/object/computed'; import { @@ -9,9 +10,9 @@ import { import WithWatchers from 'nomad-ui/mixins/with-watchers'; export default class IndexRoute extends Route.extend(WithWatchers) { + @service can; + async model() { - // Optimizing future node look ups by preemptively loading everything - await this.store.findAll('node'); return this.modelFor('jobs.job'); } @@ -30,7 +31,8 @@ export default class IndexRoute extends Route.extend(WithWatchers) { list: model.get('hasChildren') && this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), - nodes: model.get('hasClientStatus') && this.watchNodes.perform(), + nodes: + model.get('hasClientStatus') && this.can.can('read client') && this.watchNodes.perform(), }); } From c423bfe274e1341c1e36a583f5a291632f0762d9 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:45:05 -0500 Subject: [PATCH 27/37] fix: update conditional rendering of clients tab --- ui/app/components/job-subnav.js | 16 +++-- ui/app/templates/components/job-subnav.hbs | 82 +++++++++++++++++++--- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/ui/app/components/job-subnav.js b/ui/app/components/job-subnav.js index 6fb945a00c0..4d80322d86f 100644 --- a/ui/app/components/job-subnav.js +++ b/ui/app/components/job-subnav.js @@ -1,5 +1,13 @@ -import Component from '@ember/component'; -import { tagName } from '@ember-decorators/component'; +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; -@tagName('') -export default class JobSubnav extends Component {} +export default class JobSubnav extends Component { + @service can; + + get shouldRenderClientsTab() { + const { job } = this.args; + return ( + job?.hasClientStatus && !job?.hasChildren && this.can.can('read client') + ); + } +} diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index 67d9974dbc5..32050a42e4b 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -1,15 +1,79 @@
      -
    • Overview
    • -
    • Definition
    • -
    • Versions
    • +
    • + + Overview + +
    • +
    • + + Definition + +
    • +
    • + + Versions + +
    • {{#if this.job.supportsDeployments}} -
    • Deployments
    • +
    • + + Deployments + +
    • {{/if}} -
    • Allocations
    • -
    • Evaluations
    • - {{#if (and this.job.hasClientStatus (not this.job.hasChildren))}} -
    • Clients
    • +
    • + + Allocations + +
    • +
    • + + Evaluations + +
    • + {{#if this.shouldRenderClientsTab}} +
    • + + Clients + +
    • {{/if}}
    -
    + \ No newline at end of file From ecbc32723160424eb3cc06266d9389bebd536ba5 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:46:37 -0500 Subject: [PATCH 28/37] fix: update component props for glimmer syntax --- ui/app/templates/components/job-page/batch.hbs | 7 ++----- .../templates/components/job-page/parameterized-child.hbs | 2 +- ui/app/templates/components/job-page/periodic-child.hbs | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/app/templates/components/job-page/batch.hbs b/ui/app/templates/components/job-page/batch.hbs index b015faa51fc..12712b17bc8 100644 --- a/ui/app/templates/components/job-page/batch.hbs +++ b/ui/app/templates/components/job-page/batch.hbs @@ -1,14 +1,11 @@ - + - + diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 7acbeecee24..497cce60d3e 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -13,7 +13,7 @@ @model={{@job.parent}} @query={{hash namespace=@job.parent.namespace.name}} > - {{this.job.parent.name}} + {{@job.parent.name}} diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index 89f20b7f1ea..e4658462d89 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -13,7 +13,7 @@ @model={{@job.parent}} @query={{hash namespace=@job.parent.namespace.name}} > - {{this.job.parent.name}} + {{@job.parent.name}} From e634a41ea4291df7047de0d238f26e110ede0c05 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:47:11 -0500 Subject: [PATCH 29/37] fix: prevent async request for node relationship on alloc --- ui/app/utils/properties/job-client-status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/utils/properties/job-client-status.js b/ui/app/utils/properties/job-client-status.js index 8850e46ba33..a715786819f 100644 --- a/ui/app/utils/properties/job-client-status.js +++ b/ui/app/utils/properties/job-client-status.js @@ -35,7 +35,7 @@ export default function jobClientStatus(nodesKey, jobKey) { // 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'); + const nodeId = a.belongsTo('node').id(); if (!allocsByNodeID[nodeId]) { allocsByNodeID[nodeId] = []; } From e536e800e2b49a4c0c7cf4ee9a8ba277e756f8dd Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:48:06 -0500 Subject: [PATCH 30/37] feat: add conditional rendering logic to template for not auth concern --- .../parts/job-client-status-summary.hbs | 154 +++++++++++------- 1 file changed, 94 insertions(+), 60 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 e7831f45341..6d368958747 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 @@ -6,72 +6,106 @@ @startExpanded={{this.isExpanded}} @onToggle={{action this.persist}} as |a| > - -
    -
    - Job Status in Client - - {{this.jobClientStatus.totalNodes}} - - - {{x-icon "info-circle-outline" class="is-faded"}} - -
    - {{#unless a.isOpen}} -
    -
    - -
    + {{#if (can "read client")}} + +
    +
    + Job Status in Client + + {{this.jobClientStatus.totalNodes}} + + + {{x-icon "info-circle-outline" class="is-faded"}} +
    - {{/unless}} -
    -
    - - -
      - {{#each chart.data as |datum index|}} -
    1. +
      + +
      +
    + {{/unless}} +
    +
    + + +
      + {{#each chart.data as |datum index|}} +
    1. - {{#if (gt datum.value 0)}} - + {{if (eq datum.value 0) "is-empty" "is-clickable"}}" + > + {{#if (gt datum.value 0)}} + + + + {{else}} - - {{else}} - - {{/if}} -
    2. - {{/each}} -
    -
    -
    + {{/if}} + + {{/each}} + + + + {{else}} + +
    +
    + Job Status in Client + + {{x-icon "info-circle-outline" class="is-faded"}} + +
    +
    +
    + +
    +

    + Not Authorized +

    +

    + Your + + ACL token + + does not provide + + node:read + + permission. +

    +
    +
    + {{/if}} {{/if}} \ No newline at end of file From 55d1dbb3394a00e8fbb34ad8070b6247699d079c Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:48:30 -0500 Subject: [PATCH 31/37] fix: protect route if not auth --- ui/app/routes/jobs/job/clients.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/app/routes/jobs/job/clients.js b/ui/app/routes/jobs/job/clients.js index bcd3a5af341..8a4841606db 100644 --- a/ui/app/routes/jobs/job/clients.js +++ b/ui/app/routes/jobs/job/clients.js @@ -1,3 +1,4 @@ +import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; import { @@ -8,8 +9,15 @@ import { import { collect } from '@ember/object/computed'; export default class ClientsRoute extends Route.extend(WithWatchers) { + @service can; + + beforeModel() { + if (this.can.cannot('read client')) { + this.transitionTo('jobs.job'); + } + } + async model() { - await this.store.findAll('node'); return this.modelFor('jobs.job'); } From 3de0a88c57c55df23a2319f6dc18648ff1bc23b6 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:49:15 -0500 Subject: [PATCH 32/37] fix: auth node requests with mirage --- ui/mirage/config.js | 17 ++++++++++++++--- ui/mirage/factories/policy.js | 5 +++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/ui/mirage/config.js b/ui/mirage/config.js index e1ce1847c15..dda16f67f62 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -245,9 +245,20 @@ export default function() { return this.serialize(allocations.slice(0, 3)); }); - this.get('/nodes', function({ nodes }) { - const json = this.serialize(nodes.all()); - return json; + this.get('/nodes', function({ nodes }, req) { + // authorize user permissions + const { policyIds } = server.db.tokens.findBy({ + secretId: req.requestHeaders['X-Nomad-Token'], + }); + const policies = server.db.policies.find(policyIds); + const hasReadPolicy = policies.find( + p => p.rulesJSON.Node?.Policy === 'read' || p.rulesJSON.Node?.Policy === 'write' + ); + if (hasReadPolicy) { + const json = this.serialize(nodes.all()); + return json; + } + return new Response(403, {}, 'You broke everything!'); }); this.get('/node/:id'); diff --git a/ui/mirage/factories/policy.js b/ui/mirage/factories/policy.js index ea50378b13b..c01062a3b92 100644 --- a/ui/mirage/factories/policy.js +++ b/ui/mirage/factories/policy.js @@ -16,4 +16,9 @@ namespace "default" { node { policy = "read" }`, + rulesJSON: () => ({ + Node: { + Policy: 'read', + }, + }), }); From b72bf6f5a85e7eec2fa2c79eb8f3256ff85ff59a Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:50:15 -0500 Subject: [PATCH 33/37] test: add tests for not auth behavior for job-client-status-summary --- ui/tests/helpers/module-for-job.js | 199 +++++++++++++++++++---------- 1 file changed, 135 insertions(+), 64 deletions(-) diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index b929cc38367..81dc7fba507 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -1,11 +1,14 @@ /* eslint-disable qunit/require-expect */ /* eslint-disable qunit/no-conditional-assertions */ -import { currentURL } from '@ember/test-helpers'; +import { currentRouteName, currentURL, visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import JobDetail from 'nomad-ui/tests/pages/jobs/detail'; +// moduleFor is an old Ember-QUnit API that is deprected https://guides.emberjs.com/v1.10.0/testing/unit-test-helpers/ +// this is a misnomer in our context, because we're not using this API, however, the linter does not understand this +// the linter warning will go away if we rename this factory function to generateJobDetailsTests // eslint-disable-next-line ember/no-test-module-for export default function moduleForJob( title, @@ -190,6 +193,9 @@ export default function moduleForJob( }); } +// moduleFor is an old Ember-QUnit API that is deprected https://guides.emberjs.com/v1.10.0/testing/unit-test-helpers/ +// this is a misnomer in our context, because we're not using this API, however, the linter does not understand this +// the linter warning will go away if we rename this factory function to generateJobClientStatusTests // eslint-disable-next-line ember/no-test-module-for export function moduleForJobWithClientStatus( title, @@ -211,80 +217,127 @@ export function moduleForJobWithClientStatus( clients.forEach((c) => { server.create('allocation', { jobId: job.id, nodeId: c.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 - ) - ); - }); + module('with node:read permissions', function (hooks) { + hooks.beforeEach(async function () { + // Displaying the job status in client requires node:read permission. + setPolicy({ + id: 'node-read', + name: 'node-read', + rulesJSON: { + Node: { + Policy: 'read', + }, + }, + }); + + await visitJobDetailPage(job); + }); - 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('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('clicking legend item navigates to a pre-filtered clients table', async function (assert) { - const legendItem = - JobDetail.jobClientStatusSummary.legend.clickableItems[0]; - const status = legendItem.label; - await legendItem.click(); + 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' + ); + }); - 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); - }); + test('clicking legend item navigates to a pre-filtered clients table', async function (assert) { + const legendItem = + JobDetail.jobClientStatusSummary.legend.clickableItems[0]; + const status = legendItem.label; + await legendItem.click(); - 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); + }); - 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.pathname, expectedURL.pathname); + 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(); - // Sort and compare URL query params. - gotURL.searchParams.sort(); - expectedURL.searchParams.sort(); - assert.equal( - gotURL.searchParams.toString(), - expectedURL.searchParams.toString() - ); + 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.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) { + test(testName, async function (assert) { + await additionalTests[testName].call(this, job, assert); + }); + } }); - for (var testName in additionalTests) { - test(testName, async function (assert) { - await additionalTests[testName].call(this, job, assert); + module('without node:read permissions', function (hooks) { + hooks.beforeEach(async function () { + // Test blank Node policy to mock lack of permission. + setPolicy({ + id: 'node', + name: 'node', + rulesJSON: {}, + }); + + await visitJobDetailPage(job); }); - } + + test('the page handles presentations concerns regarding the user not having node:read permissions', async function (assert) { + assert + .dom("[data-test-tab='clients']") + .doesNotExist( + 'Job Detail Sub Navigation should not render Clients tab' + ); + + assert + .dom('[data-test-nodes-not-authorized]') + .exists('Renders Not Authorized message'); + }); + + test('/jobs/job/clients route is protected with authorization logic', async function (assert) { + await visit(`/jobs/${job.id}/clients`); + + assert.equal( + currentRouteName(), + 'jobs.job.index', + 'The clients route cannot be visited unless you have node:read permissions' + ); + }); + }); }); } @@ -299,3 +352,21 @@ function urlWithNamespace(url, namespace) { return `${parts[0]}?${params.toString()}`; } + +function setPolicy(policy) { + const { id: policyId } = server.create('policy', policy); + const clientToken = server.create('token', { type: 'client' }); + clientToken.policyIds = [policyId]; + clientToken.save(); + + window.localStorage.clear(); + window.localStorage.nomadTokenSecret = clientToken.secretId; +} + +async function visitJobDetailPage({ id, namespace }) { + if (!namespace || namespace === 'default') { + await JobDetail.visit({ id }); + } else { + await JobDetail.visit({ id, namespace }); + } +} From de0ac154c78afcfcd2e37fffc3f3d8924b50098d Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 5 Jan 2022 12:50:36 -0500 Subject: [PATCH 34/37] fix: no longer need gotoTaskGroup prop --- .../job-page/parts/task-groups-test.js | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/ui/tests/integration/components/job-page/parts/task-groups-test.js b/ui/tests/integration/components/job-page/parts/task-groups-test.js index 0fa42ec1f60..5d6836ec3df 100644 --- a/ui/tests/integration/components/job-page/parts/task-groups-test.js +++ b/ui/tests/integration/components/job-page/parts/task-groups-test.js @@ -1,8 +1,7 @@ import { assign } from '@ember/polyfills'; import hbs from 'htmlbars-inline-precompile'; -import { click, findAll, find, render } from '@ember/test-helpers'; +import { findAll, find, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import sinon from 'sinon'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; import { setupRenderingTest } from 'ember-qunit'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; @@ -33,7 +32,6 @@ module( job, sortProperty: 'name', sortDescending: true, - gotoTaskGroup: () => {}, }, options ); @@ -57,7 +55,7 @@ module( @job={{this.job}} @sortProperty={{this.sortProperty}} @sortDescending={{this.sortDescending}} - @gotoTaskGroup={{this.gotoTaskGroup}} /> + /> `); assert.equal( @@ -88,7 +86,6 @@ module( @job={{this.job}} @sortProperty={{this.sortProperty}} @sortDescending={{this.sortDescending}} - @gotoTaskGroup={{this.gotoTaskGroup}} /> `); const taskGroupRow = find('[data-test-task-group]'); @@ -139,41 +136,5 @@ module( 'Reserved Disk' ); }); - - test('gotoTaskGroup is called when task group rows are clicked', async function (assert) { - this.server.create('job', { - createAllocations: false, - }); - - const job = await this.store.findAll('job').then(async (jobs) => { - return await jobs.get('firstObject').reload(); - }); - - const taskGroupSpy = sinon.spy(); - - const taskGroups = await job.get('taskGroups'); - const taskGroup = taskGroups.sortBy('name').reverse().get('firstObject'); - - this.setProperties( - props(job, { - gotoTaskGroup: taskGroupSpy, - }) - ); - - await render(hbs` - - `); - - await click('[data-test-task-group]'); - - assert.ok( - taskGroupSpy.withArgs(taskGroup).calledOnce, - 'Clicking the task group row calls the gotoTaskGroup action' - ); - }); } ); From c1bb21da172704e5f334ea418224cad0f1964a99 Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 26 Jan 2022 11:28:21 -0500 Subject: [PATCH 35/37] ui: prettify remaining files --- ui/app/templates/clients/client/index.hbs | 547 +++++++++++++----- ui/app/templates/components/job-page.hbs | 18 +- .../components/job-page/parts/children.hbs | 4 +- .../components/scale-events-accordion.hbs | 34 +- .../components/job-page/parts/body-test.js | 10 +- .../components/job-page/periodic-test.js | 5 +- 6 files changed, 465 insertions(+), 153 deletions(-) diff --git a/ui/app/templates/clients/client/index.hbs b/ui/app/templates/clients/client/index.hbs index c7096c9afa9..6cb4fa0253c 100644 --- a/ui/app/templates/clients/client/index.hbs +++ b/ui/app/templates/clients/client/index.hbs @@ -5,12 +5,23 @@
    -

    Eligibility Error

    -

    {{this.eligibilityError}}

    +

    + Eligibility Error +

    +

    + {{this.eligibilityError}} +

    - +
    {{/if}} @@ -18,12 +29,23 @@
    -

    Stop Drain Error

    -

    {{this.stopDrainError}}

    +

    + Stop Drain Error +

    +

    + {{this.stopDrainError}} +

    - +
    {{/if}} @@ -31,12 +53,23 @@
    -

    Drain Error

    -

    {{this.drainError}}

    +

    + Drain Error +

    +

    + {{this.drainError}} +

    - +
    {{/if}} @@ -44,11 +77,22 @@
    -

    Drain Stopped

    -

    The drain has been stopped and the node has been set to ineligible.

    +

    + Drain Stopped +

    +

    + The drain has been stopped and the node has been set to ineligible. +

    - +
    @@ -57,11 +101,22 @@
    -

    Drain Updated

    -

    The new drain specification has been applied.

    +

    + Drain Updated +

    +

    + The new drain specification has been applied. +

    - +
    @@ -70,11 +125,22 @@
    -

    Drain Complete

    -

    Allocations have been drained and the node has been set to ineligible.

    +

    + Drain Complete +

    +

    + Allocations have been drained and the node has been set to ineligible. +

    - +
    @@ -82,7 +148,10 @@
    - + {{x-icon this.model.compositeStatusIcon}} @@ -96,15 +165,27 @@ + @isDisabled={{or + this.setEligibility.isRunning + this.model.isDraining + (cannot "write client") + }} + @onToggle={{perform this.setEligibility (not this.model.isEligible) + }} + > Eligible - + {{x-icon "info-circle-outline" class="is-faded"}} - + {{this.model.id}} @@ -119,7 +200,8 @@ @confirmText="Yes, Stop" @confirmationMessage="Are you sure you want to stop this drain?" @awaitingConfirmation={{this.stopDrain.isRunning}} - @onConfirm={{perform this.stopDrain}} /> + @onConfirm={{perform this.stopDrain}} + /> {{/if}}
    @@ -127,83 +209,135 @@ @client={{this.model}} @isDisabled={{cannot "write client"}} @onDrain={{action "drainNotify"}} - @onError={{action "setDrainError"}} /> + @onError={{action "setDrainError"}} + />
    -
    - Client Details + + Client Details + - Status - {{this.model.status}} + + Status + + + {{this.model.status}} + - Address + + Address + {{this.model.httpAddr}} - Datacenter + + Datacenter + {{this.model.datacenter}} {{#if this.model.nodeClass}} - Class + + Class + {{this.model.nodeClass}} {{/if}} - Drivers + + Drivers + {{#if this.model.unhealthyDrivers.length}} {{x-icon "alert-triangle" class="is-text is-warning"}} - {{this.model.unhealthyDrivers.length}} of {{this.model.detectedDrivers.length}} {{pluralize "driver" this.model.detectedDrivers.length}} unhealthy + {{this.model.unhealthyDrivers.length}} + of + {{this.model.detectedDrivers.length}} + {{pluralize "driver" this.model.detectedDrivers.length}} + unhealthy {{else}} All healthy {{/if}}
    - {{#if this.model.drainStrategy}}
    -
    Drain Strategy
    +
    + Drain Strategy +
    {{#unless this.model.drainStrategy.hasNoDeadline}} - Duration + + Duration + {{#if this.model.drainStrategy.isForced}} - -- + + -- + {{else}} - + {{format-duration this.model.drainStrategy.deadline}} {{/if}} {{/unless}} - {{if this.model.drainStrategy.hasNoDeadline "Deadline" "Remaining"}} + + {{if + this.model.drainStrategy.hasNoDeadline + "Deadline" + "Remaining" + }} + {{#if this.model.drainStrategy.hasNoDeadline}} - No deadline + + No deadline + {{else if this.model.drainStrategy.isForced}} - -- + + -- + {{else}} - - {{moment-from-now this.model.drainStrategy.forceDeadline interval=1000 hideAffix=true}} + + {{moment-from-now + this.model.drainStrategy.forceDeadline + interval=1000 + hideAffix=true + }} {{/if}} - Force Drain + + Force Drain + {{#if this.model.drainStrategy.isForced}} - {{x-icon "alert-triangle" class="is-text is-warning"}} Yes + {{x-icon "alert-triangle" class="is-text is-warning"}}Yes {{else}} No {{/if}} - Drain System Jobs + + Drain System Jobs + {{if this.model.drainStrategy.ignoreSystemJobs "No" "Yes"}}
    @@ -216,13 +350,15 @@ idleButton="is-warning" confirmationMessage="inherit-color" cancelButton="is-danger is-important" - confirmButton="is-warning"}} + confirmButton="is-warning" + }} @idleText="Force Drain" @cancelText="Cancel" @confirmText="Yes, Force Drain" @confirmationMessage="Are you sure you want to force drain?" @awaitingConfirmation={{this.forceDrain.isRunning}} - @onConfirm={{perform this.forceDrain}} /> + @onConfirm={{perform this.forceDrain}} + />
    {{/unless}}
    @@ -232,40 +368,65 @@
    -

    Complete

    -

    {{this.model.completeAllocations.length}}

    +

    + Complete +

    +

    + {{this.model.completeAllocations.length}} +

    -

    Migrating

    -

    {{this.model.migratingAllocations.length}}

    +

    + Migrating +

    +

    + {{this.model.migratingAllocations.length}} +

    -

    Remaining

    -

    {{this.model.runningAllocations.length}}

    +

    + Remaining +

    +

    + {{this.model.runningAllocations.length}} +

    -

    Status

    +

    + Status +

    {{#if this.model.lastMigrateTime}} -

    {{moment-to-now this.model.lastMigrateTime interval=1000 hideAffix=true}} since an allocation was successfully migrated.

    +

    + {{moment-to-now + this.model.lastMigrateTime + interval=1000 + hideAffix=true + }} + since an allocation was successfully migrated. +

    {{else}} -

    No allocations migrated.

    +

    + No allocations migrated. +

    {{/if}}
    {{/if}} -
    Host Resource Utilization - + {{x-icon "info-circle-outline" class="is-faded"}}
    @@ -280,17 +441,29 @@
    -
    Allocations - {{#if this.preemptions.length}} - {{/if}}
    @@ -325,44 +498,76 @@ />
    -
    +
    {{#if this.sortedAllocations.length}} + @page={{this.currentPage}} as |p| + > + @class="with-foot" as |t| + > - ID - Created - Modified - Status - Job - Version - Volume - CPU - Memory + + ID + + + Created + + + Modified + + + Status + + + Job + + + Version + + + Volume + + + CPU + + + Memory + + @data-test-allocation={{row.model.id}} + />
    @@ -370,15 +575,35 @@ {{else}}
    {{#if (eq this.visibleAllocations.length 0)}} -

    No Allocations

    +

    + No Allocations +

    The node doesn't have any allocations.

    {{else if this.searchTerm}} -

    No Matches

    -

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

    +

    + No Matches +

    +

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

    {{else if (eq this.sortedAllocations.length 0)}} -

    No Matches

    +

    + No Matches +

    No allocations match your current filter selection.

    @@ -387,7 +612,6 @@ {{/if}}
    -
    Client Events @@ -395,22 +619,36 @@
    - Time - Subsystem - Message + + Time + + + Subsystem + + + Message + - {{format-ts row.model.time}} - {{row.model.subsystem}} + + {{format-ts row.model.time}} + + + {{row.model.subsystem}} + {{#if row.model.message}} {{#if row.model.driver}} - {{row.model.driver}} + + {{row.model.driver}} + {{/if}} {{row.model.message}} {{else}} - No message + + No message + {{/if}} @@ -418,41 +656,65 @@
    - {{#if this.sortedHostVolumes.length}}
    Host Volumes
    - + - Name - Source - Permissions + + Name + + + Source + + + Permissions + - {{row.model.name}} - {{row.model.path}} - {{if row.model.readOnly "Read" "Read/Write"}} + + {{row.model.name}} + + + + {{row.model.path}} + + + + {{if row.model.readOnly "Read" "Read/Write"}} +
    {{/if}} -
    Driver Status
    - -
    + +
    - {{a.item.name}} + + {{a.item.name}} +
    {{#if a.item.detected}} @@ -464,13 +726,23 @@
    - Detected - {{if a.item.detected "Yes" "No"}} + + Detected + + + {{if a.item.detected "Yes" "No"}} + - Last Updated - + + Last Updated + + {{moment-from-now a.item.updateTime interval=1000}} @@ -479,21 +751,27 @@
    -

    {{a.item.healthDescription}}

    +

    + {{a.item.healthDescription}} +

    - {{capitalize a.item.name}} Attributes + {{capitalize a.item.name}} + Attributes
    {{#if a.item.attributes.structured}}
    + @class="attributes-table" + />
    {{else}}
    -

    No Driver Attributes

    +

    + No Driver Attributes +

    {{/if}} @@ -502,7 +780,6 @@
    -
    Attributes @@ -511,27 +788,33 @@ + @class="attributes-table" + />
    +
    +
    +
    + Meta
    -
    -
    - Meta + {{#if this.model.meta.structured}} +
    +
    - {{#if this.model.meta.structured}} -
    - -
    - {{else}} -
    -
    -

    No Meta Attributes

    -

    This client is configured with no meta attributes.

    -
    + {{else}} +
    +
    +

    + No Meta Attributes +

    +

    + This client is configured with no meta attributes. +

    - {{/if}} +
    + {{/if}}
    - + \ No newline at end of file diff --git a/ui/app/templates/components/job-page.hbs b/ui/app/templates/components/job-page.hbs index 54a8823964b..564524cd74c 100644 --- a/ui/app/templates/components/job-page.hbs +++ b/ui/app/templates/components/job-page.hbs @@ -1,13 +1,17 @@ {{yield (hash data=(hash) - fns=(hash) + fns=(hash setError=this.setError) ui=(hash Body=(component "job-page/parts/body" job=@job) Error=(component - "job-page/parts/error" errorMessage=this.errorMessage onDismiss=this.clearErrorMessage + "job-page/parts/error" + errorMessage=this.errorMessage + onDismiss=this.clearErrorMessage + ) + Title=(component + "job-page/parts/title" job=@job handleError=this.handleError ) - Title=(component "job-page/parts/title" job=@job handleError=this.handleError) StatsBox=(component "job-page/parts/stats-box" job=@job) Summary=(component "job-page/parts/summary" job=@job) PlacementFailures=(component "job-page/parts/placement-failures" job=@job) @@ -17,8 +21,12 @@ TaskGroups=(component "job-page/parts/task-groups" job=@job) RecentAllocations=(component "job-page/parts/recent-allocations" job=@job) Meta=(component "job-page/parts/meta" job=@job) - DasRecommendations=(component "job-page/parts/das-recommendations" job=@job) - JobClientStatusSummary=(component "job-page/parts/job-client-status-summary" job=@job) + DasRecommendations=(component + "job-page/parts/das-recommendations" job=@job + ) + JobClientStatusSummary=(component + "job-page/parts/job-client-status-summary" job=@job + ) Children=(component "job-page/parts/children" job=@job) ) ) diff --git a/ui/app/templates/components/job-page/parts/children.hbs b/ui/app/templates/components/job-page/parts/children.hbs index 89da35b6a4f..9df07ebeab6 100644 --- a/ui/app/templates/components/job-page/parts/children.hbs +++ b/ui/app/templates/components/job-page/parts/children.hbs @@ -22,7 +22,9 @@ {{/if}} {{/if}}
    -
    +
    {{#if this.sortedChildren}} - +
    - - {{#if a.item.error}}{{x-icon "cancel-circle-fill" class="is-danger"}}{{/if}} + + {{#if a.item.error}} + {{x-icon "cancel-circle-fill" class="is-danger"}} + {{/if}} + + + {{format-month-ts a.item.time}} - {{format-month-ts a.item.time}}
    {{#if a.item.hasCount}} - {{#if a.item.increased}} {{x-icon "arrow-up" class="is-danger"}} @@ -21,7 +37,9 @@ {{x-icon "arrow-down" class="is-primary"}} {{/if}} - {{a.item.count}} + + {{a.item.count}} + {{/if}}
    @@ -32,4 +50,4 @@ - + \ No newline at end of file diff --git a/ui/tests/integration/components/job-page/parts/body-test.js b/ui/tests/integration/components/job-page/parts/body-test.js index f61abc69c04..e9d6d006a05 100644 --- a/ui/tests/integration/components/job-page/parts/body-test.js +++ b/ui/tests/integration/components/job-page/parts/body-test.js @@ -48,9 +48,10 @@ module('Integration | Component | job-page/parts/body', function (hooks) { `); - const subnavLabels = findAll('[data-test-tab]').map( - (anchor) => anchor.textContent + const subnavLabels = findAll('[data-test-tab]').map((anchor) => + anchor.textContent.trim() ); + assert.ok( subnavLabels.some((label) => label === 'Definition'), 'Definition link' @@ -59,6 +60,7 @@ module('Integration | Component | job-page/parts/body', function (hooks) { subnavLabels.some((label) => label === 'Versions'), 'Versions link' ); + assert.ok( subnavLabels.some((label) => label === 'Deployments'), 'Deployments link' @@ -82,8 +84,8 @@ module('Integration | Component | job-page/parts/body', function (hooks) { `); - const subnavLabels = findAll('[data-test-tab]').map( - (anchor) => anchor.textContent + const subnavLabels = findAll('[data-test-tab]').map((anchor) => + anchor.textContent.trim() ); assert.ok( subnavLabels.some((label) => label === 'Definition'), diff --git a/ui/tests/integration/components/job-page/periodic-test.js b/ui/tests/integration/components/job-page/periodic-test.js index 54df6cd0538..fd2b3fd1c83 100644 --- a/ui/tests/integration/components/job-page/periodic-test.js +++ b/ui/tests/integration/components/job-page/periodic-test.js @@ -45,7 +45,7 @@ module('Integration | Component | job-page/periodic', function (hooks) { @sortProperty={{sortProperty}} @sortDescending={{sortDescending}} @currentPage={{currentPage}} - @gotoJob={{gotoJob}} /> + /> `; const commonProperties = (job) => ({ @@ -53,7 +53,6 @@ module('Integration | Component | job-page/periodic', function (hooks) { sortProperty: 'name', sortDescending: true, currentPage: 1, - gotoJob: () => {}, }); test('Clicking Force Launch launches a new periodic child job', async function (assert) { @@ -241,7 +240,7 @@ module('Integration | Component | job-page/periodic', function (hooks) { await render(commonTemplate); assert.equal( - find('[data-test-job-submit-time]').textContent, + find('[data-test-job-submit-time]').textContent.trim(), moment(job.get('children.firstObject.submitTime')).format( 'MMM DD HH:mm:ss ZZ' ), From 6c65966c1660e038c94a7ab45c0165b95c7ebb0f Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 26 Jan 2022 11:30:06 -0500 Subject: [PATCH 36/37] refact: fix tests after contextual job page changes --- ui/app/routes/jobs/job/index.js | 4 +- .../parts/job-client-status-summary.hbs | 2 +- .../components/job-page/parts/summary.hbs | 25 +- ui/app/templates/components/job-subnav.hbs | 2 +- .../list-accordion/accordion-head.hbs | 9 +- ui/mirage/config.js | 537 +++++++++++------- ui/tests/acceptance/job-clients-test.js | 20 + ui/tests/helpers/module-for-job.js | 18 +- .../job-page/parts/children-test.js | 53 +- .../job-page/parts/task-groups-test.js | 1 + .../pages/components/job-client-status-bar.js | 5 + ui/tests/pages/jobs/detail.js | 18 +- ui/tests/unit/utils/job-client-status-test.js | 54 +- 13 files changed, 453 insertions(+), 295 deletions(-) diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index f5b7d7b3205..be195becc5f 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -32,7 +32,9 @@ export default class IndexRoute extends Route.extend(WithWatchers) { model.get('hasChildren') && this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), nodes: - model.get('hasClientStatus') && this.can.can('read client') && this.watchNodes.perform(), + model.get('hasClientStatus') && + this.can.can('read client') && + this.watchNodes.perform(), }); } 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 6d368958747..589b8d0624e 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 @@ -1,6 +1,6 @@ {{#if this.job.hasClientStatus}} - +
    {{#if a.item.hasChildren}} @@ -32,7 +35,10 @@ {{/if}} {{else}} - + {{/if}}
    @@ -55,7 +61,10 @@ {{if (eq datum.value 0) "is-empty"}}" > - + {{/each}} @@ -83,10 +92,16 @@ @model={{this.job}} @query={{datum.legendLink.queryParams}} > - + {{else}} - + {{/if}} {{/each}} diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index 32050a42e4b..f9c11e8e6f6 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -31,7 +31,7 @@ Versions - {{#if this.job.supportsDeployments}} + {{#if @job.supportsDeployments}}
  • + \ No newline at end of file diff --git a/ui/mirage/config.js b/ui/mirage/config.js index dda16f67f62..b95244b545e 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -14,14 +14,14 @@ export function findLeader(schema) { export function filesForPath(allocFiles, filterPath) { return allocFiles.where( - file => + (file) => (!filterPath || file.path.startsWith(filterPath)) && file.path.length > filterPath.length && !file.path.substr(filterPath.length + 1).includes('/') ); } -export default function() { +export default function () { this.timing = 0; // delay for each request, automatically set to 0 during testing this.logging = window.location.search.includes('mirage-logging=true'); @@ -31,8 +31,8 @@ export default function() { const nomadIndices = {}; // used for tracking blocking queries const server = this; - const withBlockingSupport = function(fn) { - return function(schema, request) { + const withBlockingSupport = function (fn) { + return function (schema, request) { // Get the original response let { url } = request; url = url.replace(/index=\d+[&;]?/, ''); @@ -54,34 +54,44 @@ export default function() { this.get( '/jobs', - withBlockingSupport(function({ jobs }, { queryParams }) { + withBlockingSupport(function ({ jobs }, { queryParams }) { const json = this.serialize(jobs.all()); const namespace = queryParams.namespace || 'default'; return json - .filter(job => { + .filter((job) => { if (namespace === '*') return true; return namespace === 'default' ? !job.NamespaceID || job.NamespaceID === namespace : job.NamespaceID === namespace; }) - .map(job => filterKeys(job, 'TaskGroups', 'NamespaceID')); + .map((job) => filterKeys(job, 'TaskGroups', 'NamespaceID')); }) ); - this.post('/jobs', function(schema, req) { + this.post('/jobs', function (schema, req) { const body = JSON.parse(req.requestBody); - if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); + if (!body.Job) + return new Response( + 400, + {}, + 'Job is a required field on the request payload' + ); return okEmpty(); }); - this.post('/jobs/parse', function(schema, req) { + this.post('/jobs/parse', function (schema, req) { const body = JSON.parse(req.requestBody); if (!body.JobHCL) - return new Response(400, {}, 'JobHCL is a required field on the request payload'); - if (!body.Canonicalize) return new Response(400, {}, 'Expected Canonicalize to be true'); + return new Response( + 400, + {}, + 'JobHCL is a required field on the request payload' + ); + if (!body.Canonicalize) + return new Response(400, {}, 'Expected Canonicalize to be true'); // Parse the name out of the first real line of HCL to match IDs in the new job record // Regex expectation: @@ -96,13 +106,19 @@ export default function() { return new Response(200, {}, this.serialize(job)); }); - this.post('/job/:id/plan', function(schema, req) { + this.post('/job/:id/plan', function (schema, req) { const body = JSON.parse(req.requestBody); - if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); + if (!body.Job) + return new Response( + 400, + {}, + 'Job is a required field on the request payload' + ); if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true'); - const FailedTGAllocs = body.Job.Unschedulable && generateFailedTGAllocs(body.Job); + const FailedTGAllocs = + body.Job.Unschedulable && generateFailedTGAllocs(body.Job); return new Response( 200, @@ -113,13 +129,15 @@ export default function() { this.get( '/job/:id', - withBlockingSupport(function({ jobs }, { params, queryParams }) { - const job = jobs.all().models.find(job => { + withBlockingSupport(function ({ jobs }, { params, queryParams }) { + const job = jobs.all().models.find((job) => { const jobIsDefault = !job.namespaceId || job.namespaceId === 'default'; - const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default'; + const qpIsDefault = + !queryParams.namespace || queryParams.namespace === 'default'; return ( job.id === params.id && - (job.namespaceId === queryParams.namespace || (jobIsDefault && qpIsDefault)) + (job.namespaceId === queryParams.namespace || + (jobIsDefault && qpIsDefault)) ); }); @@ -127,47 +145,54 @@ export default function() { }) ); - this.post('/job/:id', function(schema, req) { + this.post('/job/:id', function (schema, req) { const body = JSON.parse(req.requestBody); - if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); + if (!body.Job) + return new Response( + 400, + {}, + 'Job is a required field on the request payload' + ); return okEmpty(); }); this.get( '/job/:id/summary', - withBlockingSupport(function({ jobSummaries }, { params }) { + withBlockingSupport(function ({ jobSummaries }, { params }) { return this.serialize(jobSummaries.findBy({ jobId: params.id })); }) ); - this.get('/job/:id/allocations', function({ allocations }, { params }) { + this.get('/job/:id/allocations', function ({ allocations }, { params }) { return this.serialize(allocations.where({ jobId: params.id })); }); - this.get('/job/:id/versions', function({ jobVersions }, { params }) { + this.get('/job/:id/versions', function ({ jobVersions }, { params }) { return this.serialize(jobVersions.where({ jobId: params.id })); }); - this.get('/job/:id/deployments', function({ deployments }, { params }) { + this.get('/job/:id/deployments', function ({ deployments }, { params }) { return this.serialize(deployments.where({ jobId: params.id })); }); - this.get('/job/:id/deployment', function({ deployments }, { params }) { + this.get('/job/:id/deployment', function ({ deployments }, { params }) { const deployment = deployments.where({ jobId: params.id }).models[0]; - return deployment ? this.serialize(deployment) : new Response(200, {}, 'null'); + return deployment + ? this.serialize(deployment) + : new Response(200, {}, 'null'); }); this.get( '/job/:id/scale', - withBlockingSupport(function({ jobScales }, { params }) { + withBlockingSupport(function ({ jobScales }, { params }) { const obj = jobScales.findBy({ jobId: params.id }); return this.serialize(jobScales.findBy({ jobId: params.id })); }) ); - this.post('/job/:id/periodic/force', function(schema, { params }) { + this.post('/job/:id/periodic/force', function (schema, { params }) { // Create the child job const parent = schema.jobs.find(params.id); @@ -182,7 +207,7 @@ export default function() { return okEmpty(); }); - this.post('/job/:id/dispatch', function(schema, { params }) { + this.post('/job/:id/dispatch', function (schema, { params }) { // Create the child job const parent = schema.jobs.find(params.id); @@ -203,7 +228,7 @@ export default function() { ); }); - this.post('/job/:id/revert', function({ jobs }, { requestBody }) { + this.post('/job/:id/revert', function ({ jobs }, { requestBody }) { const { JobID, JobVersion } = JSON.parse(requestBody); const job = jobs.find(JobID); job.version = JobVersion; @@ -212,11 +237,11 @@ export default function() { return okEmpty(); }); - this.post('/job/:id/scale', function({ jobs }, { params }) { + this.post('/job/:id/scale', function ({ jobs }, { params }) { return this.serialize(jobs.find(params.id)); }); - this.delete('/job/:id', function(schema, { params }) { + this.delete('/job/:id', function (schema, { params }) { const job = schema.jobs.find(params.id); job.update({ status: 'dead' }); return new Response(204, {}, ''); @@ -224,58 +249,70 @@ export default function() { this.get('/deployment/:id'); - this.post('/deployment/fail/:id', function() { + this.post('/deployment/fail/:id', function () { return new Response(204, {}, ''); }); - this.post('/deployment/promote/:id', function() { + this.post('/deployment/promote/:id', function () { return new Response(204, {}, ''); }); - this.get('/job/:id/evaluations', function({ evaluations }, { params }) { + this.get('/job/:id/evaluations', function ({ evaluations }, { params }) { return this.serialize(evaluations.where({ jobId: params.id })); }); this.get('/evaluation/:id'); - this.get('/deployment/allocations/:id', function(schema, { params }) { + this.get('/deployment/allocations/:id', function (schema, { params }) { const job = schema.jobs.find(schema.deployments.find(params.id).jobId); const allocations = schema.allocations.where({ jobId: job.id }); return this.serialize(allocations.slice(0, 3)); }); - this.get('/nodes', function({ nodes }, req) { + this.get('/nodes', function ({ nodes }, req) { // authorize user permissions - const { policyIds } = server.db.tokens.findBy({ + const token = server.db.tokens.findBy({ secretId: req.requestHeaders['X-Nomad-Token'], }); - const policies = server.db.policies.find(policyIds); - const hasReadPolicy = policies.find( - p => p.rulesJSON.Node?.Policy === 'read' || p.rulesJSON.Node?.Policy === 'write' - ); - if (hasReadPolicy) { - const json = this.serialize(nodes.all()); - return json; + + if (token) { + const { policyIds } = token; + const policies = server.db.policies.find(policyIds); + const hasReadPolicy = policies.find( + (p) => + p.rulesJSON.Node?.Policy === 'read' || + p.rulesJSON.Node?.Policy === 'write' + ); + if (hasReadPolicy) { + const json = this.serialize(nodes.all()); + return json; + } + return new Response(403, {}, 'Permissions have not be set-up.'); } - return new Response(403, {}, 'You broke everything!'); + + // TODO: Think about policy handling in Mirage set-up + return this.serialize(nodes.all()); }); this.get('/node/:id'); - this.get('/node/:id/allocations', function({ allocations }, { params }) { + this.get('/node/:id/allocations', function ({ allocations }, { params }) { return this.serialize(allocations.where({ nodeId: params.id })); }); - this.post('/node/:id/eligibility', function({ nodes }, { params, requestBody }) { - const body = JSON.parse(requestBody); - const node = nodes.find(params.id); + this.post( + '/node/:id/eligibility', + function ({ nodes }, { params, requestBody }) { + const body = JSON.parse(requestBody); + const node = nodes.find(params.id); - node.update({ schedulingEligibility: body.Elibility === 'eligible' }); - return this.serialize(node); - }); + node.update({ schedulingEligibility: body.Elibility === 'eligible' }); + return this.serialize(node); + } + ); - this.post('/node/:id/drain', function({ nodes }, { params }) { + this.post('/node/:id/drain', function ({ nodes }, { params }) { return this.serialize(nodes.find(params.id)); }); @@ -283,20 +320,20 @@ export default function() { this.get('/allocation/:id'); - this.post('/allocation/:id/stop', function() { + this.post('/allocation/:id/stop', function () { return new Response(204, {}, ''); }); this.get( '/volumes', - withBlockingSupport(function({ csiVolumes }, { queryParams }) { + withBlockingSupport(function ({ csiVolumes }, { queryParams }) { if (queryParams.type !== 'csi') { return new Response(200, {}, '[]'); } const json = this.serialize(csiVolumes.all()); const namespace = queryParams.namespace || 'default'; - return json.filter(volume => { + return json.filter((volume) => { if (namespace === '*') return true; return namespace === 'default' ? !volume.NamespaceID || volume.NamespaceID === namespace @@ -307,18 +344,21 @@ export default function() { this.get( '/volume/:id', - withBlockingSupport(function({ csiVolumes }, { params, queryParams }) { + withBlockingSupport(function ({ csiVolumes }, { params, queryParams }) { if (!params.id.startsWith('csi/')) { return new Response(404, {}, null); } const id = params.id.replace(/^csi\//, ''); - const volume = csiVolumes.all().models.find(volume => { - const volumeIsDefault = !volume.namespaceId || volume.namespaceId === 'default'; - const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default'; + const volume = csiVolumes.all().models.find((volume) => { + const volumeIsDefault = + !volume.namespaceId || volume.namespaceId === 'default'; + const qpIsDefault = + !queryParams.namespace || queryParams.namespace === 'default'; return ( volume.id === id && - (volume.namespaceId === queryParams.namespace || (volumeIsDefault && qpIsDefault)) + (volume.namespaceId === queryParams.namespace || + (volumeIsDefault && qpIsDefault)) ); }); @@ -326,7 +366,7 @@ export default function() { }) ); - this.get('/plugins', function({ csiPlugins }, { queryParams }) { + this.get('/plugins', function ({ csiPlugins }, { queryParams }) { if (queryParams.type !== 'csi') { return new Response(200, {}, '[]'); } @@ -334,7 +374,7 @@ export default function() { return this.serialize(csiPlugins.all()); }); - this.get('/plugin/:id', function({ csiPlugins }, { params }) { + this.get('/plugin/:id', function ({ csiPlugins }, { params }) { if (!params.id.startsWith('csi/')) { return new Response(404, {}, null); } @@ -349,7 +389,7 @@ export default function() { return this.serialize(volume); }); - this.get('/namespaces', function({ namespaces }) { + this.get('/namespaces', function ({ namespaces }) { const records = namespaces.all(); if (records.length) { @@ -359,23 +399,25 @@ export default function() { return this.serialize([{ Name: 'default' }]); }); - this.get('/namespace/:id', function({ namespaces }, { params }) { + this.get('/namespace/:id', function ({ namespaces }, { params }) { return this.serialize(namespaces.find(params.id)); }); - this.get('/agent/members', function({ agents, regions }) { + this.get('/agent/members', function ({ agents, regions }) { const firstRegion = regions.first(); return { ServerRegion: firstRegion ? firstRegion.id : null, - Members: this.serialize(agents.all()).map(({ member }) => ({ ...member })), + Members: this.serialize(agents.all()).map(({ member }) => ({ + ...member, + })), }; }); - this.get('/agent/self', function({ agents }) { + this.get('/agent/self', function ({ agents }) { return agents.first(); }); - this.get('/agent/monitor', function({ agents, nodes }, { queryParams }) { + this.get('/agent/monitor', function ({ agents, nodes }, { queryParams }) { const serverId = queryParams.server_id; const clientId = queryParams.client_id; @@ -393,11 +435,11 @@ export default function() { return logEncode(logFrames, logFrames.length - 1); }); - this.get('/status/leader', function(schema) { + this.get('/status/leader', function (schema) { return JSON.stringify(findLeader(schema)); }); - this.get('/acl/token/self', function({ tokens }, req) { + this.get('/acl/token/self', function ({ tokens }, req) { const secret = req.requestHeaders['X-Nomad-Token']; const tokenForSecret = tokens.findBy({ secretId: secret }); @@ -410,14 +452,17 @@ export default function() { return new Response(400, {}, null); }); - this.get('/acl/token/:id', function({ tokens }, req) { + this.get('/acl/token/:id', function ({ tokens }, req) { const token = tokens.find(req.params.id); const secret = req.requestHeaders['X-Nomad-Token']; const tokenForSecret = tokens.findBy({ secretId: secret }); // Return the token only if the request header matches the token // or the token is of type management - if (token.secretId === secret || (tokenForSecret && tokenForSecret.type === 'management')) { + if ( + token.secretId === secret || + (tokenForSecret && tokenForSecret.type === 'management') + ) { return this.serialize(token); } @@ -425,23 +470,26 @@ export default function() { return new Response(403, {}, null); }); - this.post('/acl/token/onetime/exchange', function({ tokens }, { requestBody }) { - const { OneTimeSecretID } = JSON.parse(requestBody); + this.post( + '/acl/token/onetime/exchange', + function ({ tokens }, { requestBody }) { + const { OneTimeSecretID } = JSON.parse(requestBody); - const tokenForSecret = tokens.findBy({ oneTimeSecret: OneTimeSecretID }); + const tokenForSecret = tokens.findBy({ oneTimeSecret: OneTimeSecretID }); - // Return the token if it exists - if (tokenForSecret) { - return { - Token: this.serialize(tokenForSecret), - }; - } + // Return the token if it exists + if (tokenForSecret) { + return { + Token: this.serialize(tokenForSecret), + }; + } - // Forbidden error if it doesn't - return new Response(403, {}, null); - }); + // Forbidden error if it doesn't + return new Response(403, {}, null); + } + ); - this.get('/acl/policy/:id', function({ policies, tokens }, req) { + this.get('/acl/policy/:id', function ({ policies, tokens }, req) { const policy = policies.find(req.params.id); const secret = req.requestHeaders['X-Nomad-Token']; const tokenForSecret = tokens.findBy({ secretId: secret }); @@ -459,7 +507,8 @@ export default function() { // is of type management if ( tokenForSecret && - (tokenForSecret.policies.includes(policy) || tokenForSecret.type === 'management') + (tokenForSecret.policies.includes(policy) || + tokenForSecret.type === 'management') ) { return this.serialize(policy); } @@ -468,11 +517,11 @@ export default function() { return new Response(403, {}, null); }); - this.get('/regions', function({ regions }) { + this.get('/regions', function ({ regions }) { return this.serialize(regions.all()); }); - this.get('/operator/license', function({ features }) { + this.get('/operator/license', function ({ features }) { const records = features.all(); if (records.length) { @@ -486,13 +535,18 @@ export default function() { return new Response(501, {}, null); }); - const clientAllocationStatsHandler = function({ clientAllocationStats }, { params }) { + const clientAllocationStatsHandler = function ( + { clientAllocationStats }, + { params } + ) { return this.serialize(clientAllocationStats.find(params.id)); }; - const clientAllocationLog = function(server, { params, queryParams }) { + const clientAllocationLog = function (server, { params, queryParams }) { const allocation = server.allocations.find(params.allocation_id); - const tasks = allocation.taskStateIds.map(id => server.taskStates.find(id)); + const tasks = allocation.taskStateIds.map((id) => + server.taskStates.find(id) + ); if (!tasks.mapBy('name').includes(queryParams.task)) { return new Response(400, {}, 'must include task name'); @@ -505,14 +559,24 @@ export default function() { return logEncode(logFrames, logFrames.length - 1); }; - const clientAllocationFSLsHandler = function({ allocFiles }, { queryParams: { path } }) { - const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path; + const clientAllocationFSLsHandler = function ( + { allocFiles }, + { queryParams: { path } } + ) { + const filterPath = path.endsWith('/') + ? path.substr(0, path.length - 1) + : path; const files = filesForPath(allocFiles, filterPath); return this.serialize(files); }; - const clientAllocationFSStatHandler = function({ allocFiles }, { queryParams: { path } }) { - const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path; + const clientAllocationFSStatHandler = function ( + { allocFiles }, + { queryParams: { path } } + ) { + const filterPath = path.endsWith('/') + ? path.substr(0, path.length - 1) + : path; // Root path if (!filterPath) { @@ -527,14 +591,20 @@ export default function() { return this.serialize(file); }; - const clientAllocationCatHandler = function({ allocFiles }, { queryParams }) { + const clientAllocationCatHandler = function ( + { allocFiles }, + { queryParams } + ) { const [file, err] = fileOrError(allocFiles, queryParams.path); if (err) return err; return file.body; }; - const clientAllocationStreamHandler = function({ allocFiles }, { queryParams }) { + const clientAllocationStreamHandler = function ( + { allocFiles }, + { queryParams } + ) { const [file, err] = fileOrError(allocFiles, queryParams.path); if (err) return err; @@ -543,14 +613,21 @@ export default function() { return file.body; }; - const clientAllocationReadAtHandler = function({ allocFiles }, { queryParams }) { + const clientAllocationReadAtHandler = function ( + { allocFiles }, + { queryParams } + ) { const [file, err] = fileOrError(allocFiles, queryParams.path); if (err) return err; return file.body.substr(queryParams.offset || 0, queryParams.limit); }; - const fileOrError = function(allocFiles, path, message = 'Operation not allowed on a directory') { + const fileOrError = function ( + allocFiles, + path, + message = 'Operation not allowed on a directory' + ) { // Root path if (path === '/') { return [null, new Response(400, {}, message)]; @@ -565,7 +642,7 @@ export default function() { }; // Client requests are available on the server and the client - this.put('/client/allocation/:id/restart', function() { + this.put('/client/allocation/:id/restart', function () { return new Response(204, {}, ''); }); @@ -578,13 +655,14 @@ export default function() { this.get('/client/fs/stream/:allocation_id', clientAllocationStreamHandler); this.get('/client/fs/readat/:allocation_id', clientAllocationReadAtHandler); - this.get('/client/stats', function({ clientStats }, { queryParams }) { + this.get('/client/stats', function ({ clientStats }, { queryParams }) { const seed = faker.random.number(10); if (seed >= 8) { const stats = clientStats.find(queryParams.node_id); stats.update({ timestamp: Date.now() * 1000000, - CPUTicksConsumed: stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }), + CPUTicksConsumed: + stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }), }); return this.serialize(stats); } else { @@ -594,134 +672,172 @@ export default function() { // TODO: in the future, this hack may be replaceable with dynamic host name // support in pretender: https://github.com/pretenderjs/pretender/issues/210 - HOSTS.forEach(host => { - this.get(`http://${host}/v1/client/allocation/:id/stats`, clientAllocationStatsHandler); - this.get(`http://${host}/v1/client/fs/logs/:allocation_id`, clientAllocationLog); + HOSTS.forEach((host) => { + this.get( + `http://${host}/v1/client/allocation/:id/stats`, + clientAllocationStatsHandler + ); + this.get( + `http://${host}/v1/client/fs/logs/:allocation_id`, + clientAllocationLog + ); - this.get(`http://${host}/v1/client/fs/ls/:allocation_id`, clientAllocationFSLsHandler); - this.get(`http://${host}/v1/client/stat/ls/:allocation_id`, clientAllocationFSStatHandler); - this.get(`http://${host}/v1/client/fs/cat/:allocation_id`, clientAllocationCatHandler); - this.get(`http://${host}/v1/client/fs/stream/:allocation_id`, clientAllocationStreamHandler); - this.get(`http://${host}/v1/client/fs/readat/:allocation_id`, clientAllocationReadAtHandler); + this.get( + `http://${host}/v1/client/fs/ls/:allocation_id`, + clientAllocationFSLsHandler + ); + this.get( + `http://${host}/v1/client/stat/ls/:allocation_id`, + clientAllocationFSStatHandler + ); + this.get( + `http://${host}/v1/client/fs/cat/:allocation_id`, + clientAllocationCatHandler + ); + this.get( + `http://${host}/v1/client/fs/stream/:allocation_id`, + clientAllocationStreamHandler + ); + this.get( + `http://${host}/v1/client/fs/readat/:allocation_id`, + clientAllocationReadAtHandler + ); - this.get(`http://${host}/v1/client/stats`, function({ clientStats }) { + this.get(`http://${host}/v1/client/stats`, function ({ clientStats }) { return this.serialize(clientStats.find(host)); }); }); - this.post('/search/fuzzy', function( - { allocations, jobs, nodes, taskGroups, csiPlugins }, - { requestBody } - ) { - const { Text } = JSON.parse(requestBody); - - const matchedAllocs = allocations.where(allocation => allocation.name.includes(Text)); - const matchedGroups = taskGroups.where(taskGroup => taskGroup.name.includes(Text)); - const matchedJobs = jobs.where(job => job.name.includes(Text)); - const matchedNodes = nodes.where(node => node.name.includes(Text)); - const matchedPlugins = csiPlugins.where(plugin => plugin.id.includes(Text)); - - const transformedAllocs = matchedAllocs.models.map(alloc => ({ - ID: alloc.name, - Scope: [alloc.namespace || 'default', alloc.id], - })); - - const transformedGroups = matchedGroups.models.map(group => ({ - ID: group.name, - Scope: [group.job.namespace, group.job.id], - })); - - const transformedJobs = matchedJobs.models.map(job => ({ - ID: job.name, - Scope: [job.namespace || 'default', job.id], - })); - - const transformedNodes = matchedNodes.models.map(node => ({ - ID: node.name, - Scope: [node.id], - })); - - const transformedPlugins = matchedPlugins.models.map(plugin => ({ - ID: plugin.id, - })); - - const truncatedAllocs = transformedAllocs.slice(0, 20); - const truncatedGroups = transformedGroups.slice(0, 20); - const truncatedJobs = transformedJobs.slice(0, 20); - const truncatedNodes = transformedNodes.slice(0, 20); - const truncatedPlugins = transformedPlugins.slice(0, 20); + this.post( + '/search/fuzzy', + function ( + { allocations, jobs, nodes, taskGroups, csiPlugins }, + { requestBody } + ) { + const { Text } = JSON.parse(requestBody); + + const matchedAllocs = allocations.where((allocation) => + allocation.name.includes(Text) + ); + const matchedGroups = taskGroups.where((taskGroup) => + taskGroup.name.includes(Text) + ); + const matchedJobs = jobs.where((job) => job.name.includes(Text)); + const matchedNodes = nodes.where((node) => node.name.includes(Text)); + const matchedPlugins = csiPlugins.where((plugin) => + plugin.id.includes(Text) + ); + + const transformedAllocs = matchedAllocs.models.map((alloc) => ({ + ID: alloc.name, + Scope: [alloc.namespace || 'default', alloc.id], + })); + + const transformedGroups = matchedGroups.models.map((group) => ({ + ID: group.name, + Scope: [group.job.namespace, group.job.id], + })); + + const transformedJobs = matchedJobs.models.map((job) => ({ + ID: job.name, + Scope: [job.namespace || 'default', job.id], + })); + + const transformedNodes = matchedNodes.models.map((node) => ({ + ID: node.name, + Scope: [node.id], + })); + + const transformedPlugins = matchedPlugins.models.map((plugin) => ({ + ID: plugin.id, + })); + + const truncatedAllocs = transformedAllocs.slice(0, 20); + const truncatedGroups = transformedGroups.slice(0, 20); + const truncatedJobs = transformedJobs.slice(0, 20); + const truncatedNodes = transformedNodes.slice(0, 20); + const truncatedPlugins = transformedPlugins.slice(0, 20); - return { - Matches: { - allocs: truncatedAllocs, - groups: truncatedGroups, - jobs: truncatedJobs, - nodes: truncatedNodes, - plugins: truncatedPlugins, - }, - Truncations: { - allocs: truncatedAllocs.length < truncatedAllocs.length, - groups: truncatedGroups.length < transformedGroups.length, - jobs: truncatedJobs.length < transformedJobs.length, - nodes: truncatedNodes.length < transformedNodes.length, - plugins: truncatedPlugins.length < transformedPlugins.length, - }, - }; - }); + return { + Matches: { + allocs: truncatedAllocs, + groups: truncatedGroups, + jobs: truncatedJobs, + nodes: truncatedNodes, + plugins: truncatedPlugins, + }, + Truncations: { + allocs: truncatedAllocs.length < truncatedAllocs.length, + groups: truncatedGroups.length < transformedGroups.length, + jobs: truncatedJobs.length < transformedJobs.length, + nodes: truncatedNodes.length < transformedNodes.length, + plugins: truncatedPlugins.length < transformedPlugins.length, + }, + }; + } + ); - this.get('/recommendations', function( - { jobs, namespaces, recommendations }, - { queryParams: { job: id, namespace } } - ) { - if (id) { - if (!namespaces.all().length) { - namespace = null; - } + this.get( + '/recommendations', + function ( + { jobs, namespaces, recommendations }, + { queryParams: { job: id, namespace } } + ) { + if (id) { + if (!namespaces.all().length) { + namespace = null; + } - const job = jobs.findBy({ id, namespace }); + const job = jobs.findBy({ id, namespace }); - if (!job) { - return []; - } + if (!job) { + return []; + } - const taskGroups = job.taskGroups.models; + const taskGroups = job.taskGroups.models; - const tasks = taskGroups.reduce((tasks, taskGroup) => { - return tasks.concat(taskGroup.tasks.models); - }, []); + const tasks = taskGroups.reduce((tasks, taskGroup) => { + return tasks.concat(taskGroup.tasks.models); + }, []); - const recommendationIds = tasks.reduce((recommendationIds, task) => { - return recommendationIds.concat(task.recommendations.models.mapBy('id')); - }, []); + const recommendationIds = tasks.reduce((recommendationIds, task) => { + return recommendationIds.concat( + task.recommendations.models.mapBy('id') + ); + }, []); - return recommendations.find(recommendationIds); - } else { - return recommendations.all(); + return recommendations.find(recommendationIds); + } else { + return recommendations.all(); + } } - }); + ); - this.post('/recommendations/apply', function({ recommendations }, { requestBody }) { - const { Apply, Dismiss } = JSON.parse(requestBody); + this.post( + '/recommendations/apply', + function ({ recommendations }, { requestBody }) { + const { Apply, Dismiss } = JSON.parse(requestBody); - Apply.concat(Dismiss).forEach(id => { - const recommendation = recommendations.find(id); - const task = recommendation.task; + Apply.concat(Dismiss).forEach((id) => { + const recommendation = recommendations.find(id); + const task = recommendation.task; - if (Apply.includes(id)) { - task.resources[recommendation.resource] = recommendation.value; - } - recommendation.destroy(); - task.save(); - }); + if (Apply.includes(id)) { + task.resources[recommendation.resource] = recommendation.value; + } + recommendation.destroy(); + task.save(); + }); - return {}; - }); + return {}; + } + ); } function filterKeys(object, ...keys) { const clone = copy(object, true); - keys.forEach(key => { + keys.forEach((key) => { delete clone[key]; }); @@ -738,7 +854,8 @@ function generateFailedTGAllocs(job, taskGroups) { const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name'); let tgNames = ['tg-one', 'tg-two']; - if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec; + if (taskGroupsFromSpec && taskGroupsFromSpec.length) + tgNames = taskGroupsFromSpec; if (taskGroups && taskGroups.length) tgNames = taskGroups; return tgNames.reduce((hash, tgName) => { diff --git a/ui/tests/acceptance/job-clients-test.js b/ui/tests/acceptance/job-clients-test.js index 4af17e15dc4..4f1ac8bde51 100644 --- a/ui/tests/acceptance/job-clients-test.js +++ b/ui/tests/acceptance/job-clients-test.js @@ -27,6 +27,16 @@ module('Acceptance | job clients', function (hooks) { setupMirage(hooks); hooks.beforeEach(function () { + setPolicy({ + id: 'node-read', + name: 'node-read', + rulesJSON: { + Node: { + Policy: 'read', + }, + }, + }); + clients = server.createList('node', 12, { datacenter: 'dc1', status: 'ready', @@ -217,3 +227,13 @@ module('Acceptance | job clients', function (hooks) { // TODO: add facet tests for actual list filtering } }); + +function setPolicy(policy) { + const { id: policyId } = server.create('policy', policy); + const clientToken = server.create('token', { type: 'client' }); + clientToken.policyIds = [policyId]; + clientToken.save(); + + window.localStorage.clear(); + window.localStorage.nomadTokenSecret = clientToken.secretId; +} diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 81dc7fba507..58d078121b3 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -1,6 +1,11 @@ /* eslint-disable qunit/require-expect */ /* eslint-disable qunit/no-conditional-assertions */ -import { currentRouteName, currentURL, visit } from '@ember/test-helpers'; +import { + click, + currentRouteName, + currentURL, + visit, +} from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -37,6 +42,11 @@ export default function moduleForJob( } else { await JobDetail.visit({ id: job.id, namespace: job.namespace }); } + + const hasClientStatus = ['system', 'sysbatch'].includes(job.type); + if (context === 'allocations' && hasClientStatus) { + await click("[data-test-accordion-summary-chart='allocation-status']"); + } }); test('visiting /jobs/:job_id', async function (assert) { @@ -248,14 +258,14 @@ export function moduleForJobWithClientStatus( test('job status summary is shown in the overview', async function (assert) { assert.ok( - JobDetail.jobClientStatusSummary.isPresent, + JobDetail.jobClientStatusSummary.statusBar.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]; + JobDetail.jobClientStatusSummary.statusBar.legend.clickableItems[0]; const status = legendItem.label; await legendItem.click(); @@ -273,7 +283,7 @@ export function moduleForJobWithClientStatus( }); test('clicking in a slice takes you to a pre-filtered clients table', async function (assert) { - const slice = JobDetail.jobClientStatusSummary.slices[0]; + const slice = JobDetail.jobClientStatusSummary.statusBar.slices[0]; const status = slice.label; await slice.click(); diff --git a/ui/tests/integration/components/job-page/parts/children-test.js b/ui/tests/integration/components/job-page/parts/children-test.js index 3c7bb98c92c..274b9ab5a5e 100644 --- a/ui/tests/integration/components/job-page/parts/children-test.js +++ b/ui/tests/integration/components/job-page/parts/children-test.js @@ -1,7 +1,6 @@ import { assign } from '@ember/polyfills'; import hbs from 'htmlbars-inline-precompile'; -import { findAll, find, click, render } from '@ember/test-helpers'; -import sinon from 'sinon'; +import { findAll, find, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; @@ -29,7 +28,6 @@ module('Integration | Component | job-page/parts/children', function (hooks) { sortProperty: 'name', sortDescending: true, currentPage: 1, - gotoJob: () => {}, }, options ); @@ -87,7 +85,7 @@ module('Integration | Component | job-page/parts/children', function (hooks) { @sortProperty={{sortProperty}} @sortDescending={{sortDescending}} @currentPage={{currentPage}} - @gotoJob={{gotoJob}} /> + /> `); const childrenCount = parent.get('children.length'); @@ -102,11 +100,12 @@ module('Integration | Component | job-page/parts/children', function (hooks) { ); assert.ok(find('.pagination-next'), 'Next button is rendered'); - assert.ok( - new RegExp(`1.10.+?${childrenCount}`).test( - find('.pagination-numbers').textContent.trim() - ) - ); + assert + .dom('.pagination-numbers') + .includesText( + '1 – 10 of 11', + 'Formats pagination to follow formula `startingIdx - endingIdx of totalTableCount' + ); await componentA11yAudit(this.element, assert); }); @@ -156,40 +155,4 @@ module('Integration | Component | job-page/parts/children', function (hooks) { ); }); }); - - test('gotoJob is called when a job row is clicked', async function (assert) { - const gotoJobSpy = sinon.spy(); - - this.server.create('job', 'periodic', { - id: 'parent', - childrenCount: 1, - createAllocations: false, - }); - - await this.store.findAll('job'); - - const parent = this.store.peekAll('job').findBy('plainId', 'parent'); - - this.setProperties( - props(parent, { - gotoJob: gotoJobSpy, - }) - ); - - await render(hbs` - - `); - - await click('tr.job-row'); - - assert.ok( - gotoJobSpy.withArgs(parent.get('children.firstObject')).calledOnce, - 'Clicking the job row calls the gotoJob action' - ); - }); }); diff --git a/ui/tests/integration/components/job-page/parts/task-groups-test.js b/ui/tests/integration/components/job-page/parts/task-groups-test.js index 5d6836ec3df..a5a44f5bc0d 100644 --- a/ui/tests/integration/components/job-page/parts/task-groups-test.js +++ b/ui/tests/integration/components/job-page/parts/task-groups-test.js @@ -86,6 +86,7 @@ module( @job={{this.job}} @sortProperty={{this.sortProperty}} @sortDescending={{this.sortDescending}} + /> `); const taskGroupRow = find('[data-test-task-group]'); diff --git a/ui/tests/pages/components/job-client-status-bar.js b/ui/tests/pages/components/job-client-status-bar.js index 6172259cc2a..83b7329022b 100644 --- a/ui/tests/pages/components/job-client-status-bar.js +++ b/ui/tests/pages/components/job-client-status-bar.js @@ -8,6 +8,11 @@ export default (scope) => ({ click: clickable(), }), + expand: { + scope: '[data-test-accordion-toggle]', + click: clickable(), + }, + legend: { scope: '.legend', diff --git a/ui/tests/pages/jobs/detail.js b/ui/tests/pages/jobs/detail.js index 0cf45eb2626..9c07feaa727 100644 --- a/ui/tests/pages/jobs/detail.js +++ b/ui/tests/pages/jobs/detail.js @@ -75,16 +75,22 @@ export default create({ return this.packStats.toArray().findBy('id', id); }, - jobClientStatusSummary: jobClientStatusBar( - '[data-test-job-client-status-bar]' - ), - childrenSummary: isPresent( + jobClientStatusSummary: { + scope: '[data-test-job-client-summary]', + statusBar: jobClientStatusBar('[data-test-job-client-status-bar]'), + toggle: { + scope: '[data-test-accordion-head] [data-test-accordion-toggle]', + click: clickable(), + isDisabled: attribute('disabled'), + tooltip: attribute('aria-label'), + }, + }, + childrenSummary: jobClientStatusBar( '[data-test-job-summary] [data-test-children-status-bar]' ), - allocationsSummary: isPresent( + allocationsSummary: jobClientStatusBar( '[data-test-job-summary] [data-test-allocation-status-bar]' ), - ...allocations(), viewAllAllocations: text('[data-test-view-all-allocations]'), diff --git a/ui/tests/unit/utils/job-client-status-test.js b/ui/tests/unit/utils/job-client-status-test.js index 0c3d88465ab..e625a4f5730 100644 --- a/ui/tests/unit/utils/job-client-status-test.js +++ b/ui/tests/unit/utils/job-client-status-test.js @@ -35,6 +35,22 @@ class NodeMock { } } +class AllocationMock { + constructor(node, clientStatus) { + this.node = node; + this.clientStatus = clientStatus; + } + + belongsTo() { + const self = this; + return { + id() { + return self.node.id; + }, + }; + } +} + 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'); @@ -42,7 +58,7 @@ module('Unit | Util | JobClientStatus', function () { const job = { datacenters: ['dc1'], status: 'running', - allocations: [{ node, clientStatus: 'running' }], + allocations: [new AllocationMock(node, 'running')], taskGroups: [{}], }; const expected = { @@ -75,9 +91,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node, clientStatus: 'running' }, - { node, clientStatus: 'failed' }, - { node, clientStatus: 'running' }, + new AllocationMock(node, 'running'), + new AllocationMock(node, 'failed'), + new AllocationMock(node, 'running'), ], taskGroups: [{}, {}, {}], }; @@ -111,9 +127,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node, clientStatus: 'lost' }, - { node, clientStatus: 'lost' }, - { node, clientStatus: 'lost' }, + new AllocationMock(node, 'lost'), + new AllocationMock(node, 'lost'), + new AllocationMock(node, 'lost'), ], taskGroups: [{}, {}, {}], }; @@ -147,9 +163,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node, clientStatus: 'failed' }, - { node, clientStatus: 'failed' }, - { node, clientStatus: 'failed' }, + new AllocationMock(node, 'failed'), + new AllocationMock(node, 'failed'), + new AllocationMock(node, 'failed'), ], taskGroups: [{}, {}, {}], }; @@ -183,9 +199,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node, clientStatus: 'running' }, - { node, clientStatus: 'running' }, - { node, clientStatus: 'running' }, + new AllocationMock(node, 'running'), + new AllocationMock(node, 'running'), + new AllocationMock(node, 'running'), ], taskGroups: [{}, {}, {}, {}], }; @@ -251,9 +267,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'pending', allocations: [ - { node, clientStatus: 'starting' }, - { node, clientStatus: 'starting' }, - { node, clientStatus: 'starting' }, + new AllocationMock(node, 'starting'), + new AllocationMock(node, 'starting'), + new AllocationMock(node, 'starting'), ], taskGroups: [{}, {}, {}, {}], }; @@ -288,9 +304,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node: node1, clientStatus: 'running' }, - { node: node2, clientStatus: 'failed' }, - { node: node1, clientStatus: 'running' }, + new AllocationMock(node1, 'running'), + new AllocationMock(node2, 'failed'), + new AllocationMock(node1, 'running'), ], taskGroups: [{}, {}], }; From 8d8fe0bd2d87bb8200fd69b697ce29ac73f534da Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 26 Jan 2022 12:06:18 -0500 Subject: [PATCH 37/37] refact: extract setPolicy into utils --- ui/tests/acceptance/job-clients-test.js | 11 +---------- ui/tests/helpers/module-for-job.js | 11 +---------- ui/tests/utils/set-policy.js | 9 +++++++++ 3 files changed, 11 insertions(+), 20 deletions(-) create mode 100644 ui/tests/utils/set-policy.js diff --git a/ui/tests/acceptance/job-clients-test.js b/ui/tests/acceptance/job-clients-test.js index 4f1ac8bde51..528a1565377 100644 --- a/ui/tests/acceptance/job-clients-test.js +++ b/ui/tests/acceptance/job-clients-test.js @@ -5,6 +5,7 @@ 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'; +import setPolicy from 'nomad-ui/tests/utils/set-policy'; let job; let clients; @@ -227,13 +228,3 @@ module('Acceptance | job clients', function (hooks) { // TODO: add facet tests for actual list filtering } }); - -function setPolicy(policy) { - const { id: policyId } = server.create('policy', policy); - const clientToken = server.create('token', { type: 'client' }); - clientToken.policyIds = [policyId]; - clientToken.save(); - - window.localStorage.clear(); - window.localStorage.nomadTokenSecret = clientToken.secretId; -} diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 58d078121b3..53d373ae812 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -10,6 +10,7 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import JobDetail from 'nomad-ui/tests/pages/jobs/detail'; +import setPolicy from 'nomad-ui/tests/utils/set-policy'; // moduleFor is an old Ember-QUnit API that is deprected https://guides.emberjs.com/v1.10.0/testing/unit-test-helpers/ // this is a misnomer in our context, because we're not using this API, however, the linter does not understand this @@ -363,16 +364,6 @@ function urlWithNamespace(url, namespace) { return `${parts[0]}?${params.toString()}`; } -function setPolicy(policy) { - const { id: policyId } = server.create('policy', policy); - const clientToken = server.create('token', { type: 'client' }); - clientToken.policyIds = [policyId]; - clientToken.save(); - - window.localStorage.clear(); - window.localStorage.nomadTokenSecret = clientToken.secretId; -} - async function visitJobDetailPage({ id, namespace }) { if (!namespace || namespace === 'default') { await JobDetail.visit({ id }); diff --git a/ui/tests/utils/set-policy.js b/ui/tests/utils/set-policy.js new file mode 100644 index 00000000000..49915b6f5fe --- /dev/null +++ b/ui/tests/utils/set-policy.js @@ -0,0 +1,9 @@ +export default function setPolicy(policy) { + const { id: policyId } = server.create('policy', policy); + const clientToken = server.create('token', { type: 'client' }); + clientToken.policyIds = [policyId]; + clientToken.save(); + + window.localStorage.clear(); + window.localStorage.nomadTokenSecret = clientToken.secretId; +}