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 deleted file mode 100644 index c6bd835e4..000000000 --- a/ui/app/components/job-page/parts/job-client-status-summary.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import Component from '@ember/component'; -import { action, computed } from '@ember/object'; -import { inject as service } from '@ember/service'; -import { camelize } from '@ember/string'; -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 router; - @service store; - - @jobClientStatus('nodes', 'job') jobClientStatus; - - get nodes() { - return this.store.peekAll('node'); - } - - job = null; - - @action - gotoClients(statusFilter) { - this.router.transitionTo('jobs.job.clients', this.job, { - queryParams: { - status: JSON.stringify(statusFilter), - }, - }); - } - - @computed - get isExpanded() { - const storageValue = window.localStorage.nomadExpandJobClientStatusSummary; - return storageValue != null ? JSON.parse(storageValue) : true; - } - - @action - onSliceClick(ev, slice) { - this.gotoClients([camelize(slice.className)]); - } - - persist(item, isOpen) { - window.localStorage.nomadExpandJobClientStatusSummary = isOpen; - this.notifyPropertyChange('isExpanded'); - } -} diff --git a/ui/app/components/job-status/panel/steady.js b/ui/app/components/job-status/panel/steady.js index 6947d8d9a..2ad0c2351 100644 --- a/ui/app/components/job-status/panel/steady.js +++ b/ui/app/components/job-status/panel/steady.js @@ -76,7 +76,21 @@ export default class JobStatusPanelSteadyComponent extends Component { .filter( (a) => a.clientStatus !== 'running' && a.clientStatus !== 'pending' ) - .sortBy('jobVersion') + .sort((a, b) => { + // First sort by jobVersion + if (a.jobVersion > b.jobVersion) return 1; + if (a.jobVersion < b.jobVersion) return -1; + + // If jobVersion is the same, sort by status order + if (a.jobVersion === b.jobVersion) { + return ( + jobAllocStatuses[this.args.job.type].indexOf(b.clientStatus) - + jobAllocStatuses[this.args.job.type].indexOf(a.clientStatus) + ); + } else { + return 0; + } + }) .reverse(); // Iterate over the sorted allocs @@ -137,7 +151,7 @@ export default class JobStatusPanelSteadyComponent extends Component { } get atMostOneAllocPerNode() { - return this.args.job.type === 'system'; + return this.args.job.type === 'system' || this.args.job.type === 'sysbatch'; } get versions() { diff --git a/ui/app/templates/components/job-page.hbs b/ui/app/templates/components/job-page.hbs index 8ed07751b..ed7ecfce3 100644 --- a/ui/app/templates/components/job-page.hbs +++ b/ui/app/templates/components/job-page.hbs @@ -26,9 +26,6 @@ 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) StatusPanel=(component diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 8d455f161..f05ad66f8 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -22,8 +22,7 @@ - - + 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 deleted file mode 100644 index 524c3dd3c..000000000 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ /dev/null @@ -1,116 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: MPL-2.0 -~}} - -{{#if this.job.hasClientStatus}} - - {{#if (can "read client")}} - - - - Job Status in Client - - {{this.jobClientStatus.totalNodes}} - - - {{x-icon "info-circle-outline" class="is-faded"}} - - - {{#unless a.isOpen}} - - - - - - {{/unless}} - - - - - - {{#each chart.data as |datum index|}} - - {{#if (gt datum.value 0)}} - - - - {{else}} - - {{/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 diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index af8c37d45..9066e9e01 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -22,8 +22,7 @@ - - + diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs index e8344327b..763098932 100644 --- a/ui/app/templates/components/job-page/sysbatch.hbs +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -8,8 +8,7 @@ - - + diff --git a/ui/app/utils/allocation-client-statuses.js b/ui/app/utils/allocation-client-statuses.js index b4311f6d3..443195403 100644 --- a/ui/app/utils/allocation-client-statuses.js +++ b/ui/app/utils/allocation-client-statuses.js @@ -11,7 +11,8 @@ export const jobAllocStatuses = { service: ['running', 'pending', 'failed', 'lost', 'unplaced'], system: ['running', 'pending', 'failed', 'lost', 'unplaced'], - batch: ['running', 'pending', 'failed', 'lost', 'complete', 'unplaced'], + batch: ['running', 'pending', 'complete', 'failed', 'lost', 'unplaced'], + sysbatch: ['running', 'pending', 'complete', 'failed', 'lost', 'unplaced'], }; -export const jobTypes = ['service', 'system', 'batch']; +export const jobTypes = ['service', 'system', 'batch', 'sysbatch']; diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index e61b914db..9493694b6 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -345,6 +345,7 @@ export default Factory.extend({ datacenters: job.datacenters, createAllocations: job.createAllocations, shallow: job.shallow, + noActiveDeployment: job.noActiveDeployment, }); } diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index 9ee398122..e24a48af9 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -33,7 +33,11 @@ moduleForJob('Acceptance | job detail (system)', 'allocations', () => ); moduleForJob('Acceptance | job detail (sysbatch)', 'allocations', () => - server.create('job', { type: 'sysbatch', shallow: true }) + server.create('job', { + type: 'sysbatch', + shallow: true, + noActiveDeployment: true, + }) ); moduleForJobWithClientStatus( @@ -44,6 +48,7 @@ moduleForJobWithClientStatus( datacenters: ['dc1'], type: 'sysbatch', createAllocations: false, + noActiveDeployment: true, }) ); @@ -57,6 +62,7 @@ moduleForJobWithClientStatus( type: 'sysbatch', namespaceId: namespace.name, createAllocations: false, + noActiveDeployment: true, }); } ); @@ -71,6 +77,7 @@ moduleForJobWithClientStatus( type: 'sysbatch', namespaceId: namespace.name, createAllocations: false, + noActiveDeployment: true, }); } ); @@ -80,6 +87,7 @@ moduleForJob('Acceptance | job detail (sysbatch child)', 'allocations', () => { childrenCount: 1, shallow: true, datacenters: ['dc1'], + noActiveDeployment: true, }); return server.db.jobs.where({ parentId: parent.id })[0]; }); @@ -91,6 +99,7 @@ moduleForJobWithClientStatus( childrenCount: 1, shallow: true, datacenters: ['dc1'], + noActiveDeployment: true, }); return server.db.jobs.where({ parentId: parent.id })[0]; } @@ -105,6 +114,7 @@ moduleForJobWithClientStatus( shallow: true, namespaceId: namespace.name, datacenters: ['dc1'], + noActiveDeployment: true, }); return server.db.jobs.where({ parentId: parent.id })[0]; } @@ -119,6 +129,7 @@ moduleForJobWithClientStatus( shallow: true, namespaceId: namespace.name, datacenters: ['*'], + noActiveDeployment: true, }); return server.db.jobs.where({ parentId: parent.id })[0]; } @@ -168,7 +179,11 @@ moduleForJob( moduleForJob( 'Acceptance | job detail (parameterized)', 'children', - () => server.create('job', 'parameterized', { shallow: true }), + () => + server.create('job', 'parameterized', { + shallow: true, + noActiveDeployment: true, + }), { 'the default sort is submitTime descending': async (job, assert) => { const mostRecentLaunch = server.db.jobs @@ -221,6 +236,7 @@ moduleForJob( const parent = server.create('job', 'parameterized', { childrenCount: 1, shallow: true, + noActiveDeployment: true, }); return server.db.jobs.where({ parentId: parent.id })[0]; } diff --git a/ui/tests/acceptance/job-status-panel-test.js b/ui/tests/acceptance/job-status-panel-test.js index 13dac39cc..f8322b667 100644 --- a/ui/tests/acceptance/job-status-panel-test.js +++ b/ui/tests/acceptance/job-status-panel-test.js @@ -254,6 +254,93 @@ module('Acceptance | job status panel', function (hooks) { }); }); + test('After running/pending allocations are covered, fill in allocs by jobVersion, descending (batch)', async function (assert) { + assert.expect(7); + let job = server.create('job', { + status: 'running', + datacenters: ['*'], + type: 'batch', + resourceSpec: ['M: 256, C: 500'], // a single group + createAllocations: false, + allocStatusDistribution: { + running: 0.5, + failed: 0.3, + unknown: 0, + lost: 0, + complete: 0.2, + }, + groupTaskCount: 5, + shallow: true, + version: 5, + noActiveDeployment: true, + }); + + server.create('allocation', { + jobId: job.id, + clientStatus: 'running', + jobVersion: 5, + }); + server.create('allocation', { + jobId: job.id, + clientStatus: 'pending', + jobVersion: 5, + }); + server.create('allocation', { + jobId: job.id, + clientStatus: 'running', + jobVersion: 3, + }); + server.create('allocation', { + jobId: job.id, + clientStatus: 'failed', + jobVersion: 4, + }); + server.create('allocation', { + jobId: job.id, + clientStatus: 'complete', + jobVersion: 4, + }); + server.create('allocation', { + jobId: job.id, + clientStatus: 'lost', + jobVersion: 5, + }); + + await visit(`/jobs/${job.id}`); + assert.dom('.job-status-panel').exists(); + // We expect to see 5 represented-allocations, since that's the number in our groupTaskCount + assert + .dom('.ungrouped-allocs .represented-allocation') + .exists({ count: 5 }); + + // We expect 2 of them to be running, and one to be pending, since running/pending allocations superecede other clientStatuses + assert + .dom('.ungrouped-allocs .represented-allocation.running') + .exists({ count: 2 }); + assert + .dom('.ungrouped-allocs .represented-allocation.pending') + .exists({ count: 1 }); + + // We expect 1 to be lost, since it has the highest jobVersion + assert + .dom('.ungrouped-allocs .represented-allocation.lost') + .exists({ count: 1 }); + + // We expect the remaining one to be complete, rather than failed, since it comes earlier in the jobAllocStatuses.batch constant + assert + .dom('.ungrouped-allocs .represented-allocation.complete') + .exists({ count: 1 }); + assert + .dom('.ungrouped-allocs .represented-allocation.failed') + .doesNotExist(); + + await percySnapshot(assert, { + percyCSS: ` + .allocation-row td { display: none; } + `, + }); + }); + test('Status Panel groups allocations when they get past a threshold', async function (assert) { assert.expect(6); diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index ee1785236..73a84267c 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -5,21 +5,14 @@ /* eslint-disable qunit/require-expect */ /* eslint-disable qunit/no-conditional-assertions */ -import { - click, - currentRouteName, - currentURL, - visit, - find, -} from '@ember/test-helpers'; +import { currentRouteName, currentURL, visit, find } 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'; import setPolicy from 'nomad-ui/tests/utils/set-policy'; -const jobTypesWithStatusPanel = ['service', 'system', 'batch']; - +const jobTypesWithStatusPanel = ['service', 'system', 'batch', 'sysbatch']; async function switchToHistorical() { await JobDetail.statusModes.historical.click(); } @@ -55,11 +48,6 @@ export default function moduleForJob( } else { await JobDetail.visit({ id: `${job.id}@${job.namespace}` }); } - - const hasClientStatus = ['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) { @@ -117,7 +105,7 @@ export default function moduleForJob( if (context === 'allocations') { test('allocations for the job are shown in the overview', async function (assert) { - if (!job.parentId && jobTypesWithStatusPanel.includes(job.type)) { + if (jobTypesWithStatusPanel.includes(job.type)) { await switchToHistorical(job); } assert.ok( @@ -157,7 +145,7 @@ export default function moduleForJob( }); test('clicking legend item navigates to a pre-filtered allocations table', async function (assert) { - if (!job.parentId && jobTypesWithStatusPanel.includes(job.type)) { + if (jobTypesWithStatusPanel.includes(job.type)) { await switchToHistorical(job); } const legendItem = find('.legend li.is-clickable'); @@ -178,7 +166,7 @@ export default function moduleForJob( }); test('clicking in a slice takes you to a pre-filtered allocations table', async function (assert) { - if (!job.parentId && jobTypesWithStatusPanel.includes(job.type)) { + if (jobTypesWithStatusPanel.includes(job.type)) { await switchToHistorical(job); } const slice = JobDetail.allocationsSummary.slices[0]; @@ -301,46 +289,6 @@ export function moduleForJobWithClientStatus( assert.equal(currentURL(), expectedURL); }); - test('job status summary is shown in the overview', async function (assert) { - assert.ok( - 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.statusBar.legend.clickableItems[0]; - const status = legendItem.label; - await legendItem.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); - }); - - test('clicking in a slice takes you to a pre-filtered clients table', async function (assert) { - const slice = JobDetail.jobClientStatusSummary.statusBar.slices[0]; - const status = slice.label; - await slice.click(); - - const encodedStatus = encodeURIComponent(JSON.stringify([status])); - - const expectedURL = job.namespace - ? `/jobs/${job.name}@${job.namespace}/clients?status=${encodedStatus}` - : `/jobs/${job.name}/clients?status=${encodedStatus}`; - - assert.deepEqual(currentURL(), expectedURL, 'url is correct'); - }); - for (var testName in additionalTests) { test(testName, async function (assert) { await additionalTests[testName].call(this, job, assert); @@ -366,10 +314,6 @@ export function moduleForJobWithClientStatus( .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) { diff --git a/ui/tests/pages/jobs/detail.js b/ui/tests/pages/jobs/detail.js index c7e41b411..471b37bb6 100644 --- a/ui/tests/pages/jobs/detail.js +++ b/ui/tests/pages/jobs/detail.js @@ -93,16 +93,6 @@ export default create({ }, }, - 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-children-status-bar]:not(.is-narrow)' ),
- Your - - ACL token - - does not provide - - node:read - - permission. -
- node:read -