[ui, deployments] Show a "Latest Deployment Status" cell within the Job Status panel on steady service jobs (#17246)

* Failed or lost cell condensed

* Latest Deployment cell

* Stylistic changes and deploying state fixup

* Rewritten tooltip message and updated lost/failed tests

* failed-or-lost cell updates to job status panel acceptance tests
This commit is contained in:
Phil Renaud 2023-05-25 14:20:48 -04:00 committed by GitHub
parent e04d8cf77b
commit 98aa88c739
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 205 additions and 104 deletions

View File

@ -1,36 +1,44 @@
<section class="failed-or-lost">
<h4>
{{@title}}
<span
class="tooltip multiline text-center"
role="tooltip"
aria-label={{@description}}
>
<FlightIcon @name="info" />
<h4>Replaced Allocations</h4>
<div class="failed-or-lost-links">
{{#if @supportsRescheduling}}
<span>
<span
class="tooltip multiline text-center"
role="tooltip"
aria-label="Allocations that have been rescheduled, on another node if possible, due to failure or manual restart"
>
<FlightIcon @name="info" />
</span>
<ConditionalLinkTo
@condition={{@rescheduledAllocs.length}}
@route="jobs.job.allocations"
@model={{@job}}
@query={{hash scheduling='["has-been-rescheduled"]' version=(concat '[' @job.latestDeployment.versionNumber ']')}}
@label="View Allocations"
>
{{@rescheduledAllocs.length}} Rescheduled
</ConditionalLinkTo>
</span>
{{/if}}
<span>
<span
class="tooltip multiline text-center"
role="tooltip"
aria-label="Allocations that have been restarted in-place due to a task failure or manual restart"
>
<FlightIcon @name="info" />
</span>
<ConditionalLinkTo
@condition={{@restartedAllocs.length}}
@route="jobs.job.allocations"
@model={{@job}}
@query={{hash scheduling='["has-been-restarted"]' version=(concat '[' @job.latestDeployment.versionNumber ']')}}
@label="View Allocations"
>
{{@restartedAllocs.length}} Restarted
</ConditionalLinkTo>
</span>
</h4>
{{#if (eq @title "Rescheduled")}}
<ConditionalLinkTo
@condition={{this.shouldLinkToAllocations}}
@route="jobs.job.allocations"
@model={{@job}}
@query={{hash scheduling='["has-been-rescheduled"]' version=(concat '[' @job.latestDeployment.versionNumber ']')}}
@label="View Allocations"
@class="failed-or-lost-link"
>
{{@allocs.length}}
</ConditionalLinkTo>
{{/if}}
{{#if (eq @title "Restarted")}}
<ConditionalLinkTo
@condition={{this.shouldLinkToAllocations}}
@route="jobs.job.allocations"
@model={{@job}}
@query={{hash scheduling='["has-been-restarted"]' version=(concat '[' @job.latestDeployment.versionNumber ']')}}
@label="View Allocations"
@class="failed-or-lost-link"
>
{{@allocs.length}}
</ConditionalLinkTo>
{{/if}}
</div>
</section>

View File

@ -1,7 +0,0 @@
import Component from '@glimmer/component';
export default class JobStatusFailedOrLostComponent extends Component {
get shouldLinkToAllocations() {
return this.args.allocs.length;
}
}

View File

@ -0,0 +1,10 @@
<section class="latest-deployment">
<LinkTo @route="jobs.job.deployments" @model={{@job}}>
<h4>
Latest Deployment
<FlightIcon @name="arrow-right" />
</h4>
</LinkTo>
<Hds::Badge @text={{capitalize this.status}} @size="small" @color={{this.statusColor}} @type="filled" />
<p>{{this.healthyAllocs}}/{{this.desiredTotal}} Healthy</p>
</section>

View File

@ -0,0 +1,31 @@
import Component from '@glimmer/component';
import { alias } from '@ember/object/computed';
export default class JobStatusLatestDeploymentComponent extends Component {
@alias('args.job.latestDeployment') deployment;
@alias('deployment.status') status;
get healthyAllocs() {
return this.deployment
.get('taskGroupSummaries')
.mapBy('healthyAllocs')
.reduce((sum, count) => sum + count, 0);
}
get desiredTotal() {
return this.deployment
.get('taskGroupSummaries')
.mapBy('desiredTotal')
.reduce((sum, count) => sum + count, 0);
}
get statusColor() {
switch (this.status) {
case 'successful':
return 'success';
case 'failed':
return 'critical';
default:
return 'neutral';
}
}
}

View File

@ -104,17 +104,10 @@
</legend>
<JobStatus::FailedOrLost
@allocs={{this.rescheduledAllocs}}
@rescheduledAllocs={{this.rescheduledAllocs}}
@restartedAllocs={{this.restartedAllocs}}
@job={{@job}}
@title="Rescheduled"
@description="Allocations that have been rescheduled, on another node if possible, due to failure during deployment"
/>
<JobStatus::FailedOrLost
@allocs={{this.restartedAllocs}}
@job={{@job}}
@title="Restarted"
@description="Allocations that have been restarted in-place due to a task failure during deployment"
@supportsRescheduling={{true}}
/>
</div>

View File

@ -41,7 +41,7 @@
</h3>
<JobStatus::AllocationStatusRow @allocBlocks={{this.allocBlocks}} @steady={{true}} />
<div class="legend-and-summary">
<div class="legend-and-summary {{if @job.latestDeployment "has-latest-deployment"}}">
<legend>
{{#each this.allocTypes as |type|}}
<ConditionalLinkTo
@ -58,20 +58,12 @@
{{/each}}
</legend>
{{#if this.supportsRescheduling}}
<JobStatus::FailedOrLost
@allocs={{this.rescheduledAllocs}}
@job={{@job}}
@title="Rescheduled"
@description="Allocations that have been rescheduled, on another node if possible, due to failure"
/>
{{/if}}
<JobStatus::FailedOrLost
@allocs={{this.restartedAllocs}}
@rescheduledAllocs={{this.rescheduledAllocs}}
@restartedAllocs={{this.restartedAllocs}}
@job={{@job}}
@title="Restarted"
@description="Allocations that have been restarted in-place due to a task failure"
@supportsRescheduling={{this.supportsRescheduling}}
/>
<section class="versions">
@ -92,6 +84,10 @@
</ul>
</section>
{{#if @job.latestDeployment}}
<JobStatus::LatestDeployment @job={{@job}} />
{{/if}}
</div>
<div class="history-and-params">

View File

@ -48,11 +48,15 @@
// grid-area: legend-and-summary;
// TODO: may revisit this grid-area later, but is currently used in 2 competing ways
display: grid;
gap: 0.5rem;
grid-template-columns: 55% 15% 15% 15%;
gap: 1rem;
grid-template-columns: 3fr 1fr 1fr;
&.has-latest-deployment {
grid-template-columns: 3fr 1fr 1fr 1fr;
}
& > section > h4,
& > legend > h4 {
& > legend > h4,
& > section > a > h4 {
margin-bottom: 0.5rem;
}
@ -62,8 +66,6 @@
gap: 0.5rem;
}
.versions {
display: grid;
gap: 0.5rem;
& > ul {
display: grid;
grid-template-columns: repeat(auto-fit, 100px);
@ -76,12 +78,18 @@
}
}
}
.latest-deployment {
h4 svg {
position: relative;
top: 3px;
}
}
.failed-or-lost {
.failed-or-lost-link {
display: block;
font-size: 1.5rem;
font-weight: bold;
.failed-or-lost > div {
display: grid;
gap: 3px;
.tooltip {
top: 3px;
}
}
}

View File

@ -648,29 +648,29 @@ module('Acceptance | job status panel', function (hooks) {
await visit(`/jobs/${job.id}`);
assert.dom('.job-status-panel').exists();
assert
.dom('.failed-or-lost')
.dom('.failed-or-lost-links > span')
.exists({ count: 2 }, 'Restarted and Rescheduled cells are both present');
let rescheduledCell = [...findAll('.failed-or-lost')][0];
let restartedCell = [...findAll('.failed-or-lost')][1];
// await this.pauseTest();
let rescheduledCell = [...findAll('.failed-or-lost-links > span')][0];
let restartedCell = [...findAll('.failed-or-lost-links > span')][1];
// Check that the title in each cell has the right text
assert.dom(rescheduledCell.querySelector('h4')).hasText('Rescheduled');
assert.dom(restartedCell.querySelector('h4')).hasText('Restarted');
assert.dom(rescheduledCell).hasText('0 Rescheduled');
assert.dom(restartedCell).hasText('0 Restarted');
// Check that both values are zero and non-links
assert
.dom(rescheduledCell.querySelector('a'))
.doesNotExist('Rescheduled cell is not a link');
assert
.dom(rescheduledCell.querySelector('.failed-or-lost-link'))
.hasText('0', 'Rescheduled cell has zero value');
.dom(rescheduledCell)
.hasText('0 Rescheduled', 'Rescheduled cell has zero value');
assert
.dom(restartedCell.querySelector('a'))
.doesNotExist('Restarted cell is not a link');
assert
.dom(restartedCell.querySelector('.failed-or-lost-link'))
.hasText('0', 'Restarted cell has zero value');
.dom(restartedCell)
.hasText('0 Restarted', 'Restarted cell has zero value');
// A wild event appears! Change a recent task event to type "Restarting" in a task state:
this.store
@ -687,9 +687,9 @@ module('Acceptance | job status panel', function (hooks) {
await settled();
assert
.dom(restartedCell.querySelector('.failed-or-lost-link'))
.dom(restartedCell)
.hasText(
'1',
'1 Restarted',
'Restarted cell updates when a task event with type "Restarting" is added'
);
@ -708,9 +708,9 @@ module('Acceptance | job status panel', function (hooks) {
// Trigger a reschedule! Set up a desiredTransition object with a Reschedule property on one of the allocations.
assert
.dom(restartedCell.querySelector('.failed-or-lost-link'))
.dom(restartedCell)
.hasText(
'2',
'2 Restarted',
'Restarted cell updates when a second task event with type "Restarting" is added'
);
@ -725,8 +725,11 @@ module('Acceptance | job status panel', function (hooks) {
await settled();
assert
.dom(rescheduledCell.querySelector('.failed-or-lost-link'))
.hasText('1', 'Rescheduled cell updates when desiredTransition is set');
.dom(rescheduledCell)
.hasText(
'1 Rescheduled',
'Rescheduled cell updates when desiredTransition is set'
);
assert
.dom(rescheduledCell.querySelector('a'))
.exists('Rescheduled cell with a non-zero number is now a link');
@ -900,10 +903,10 @@ module('Acceptance | job status panel', function (hooks) {
await visit(`/jobs/${job.id}`);
assert.dom('.job-status-panel').exists();
assert.dom('.failed-or-lost').exists({ count: 1 });
assert.dom('.failed-or-lost h4').hasText('Restarted');
assert.dom('.failed-or-lost h4').hasText('Replaced Allocations');
assert
.dom('.failed-or-lost-link')
.hasText('0', 'Restarted cell at zero by default');
.dom('.failed-or-lost-links > span')
.hasText('0 Restarted', 'Restarted cell at zero by default');
// A wild event appears! Change a recent task event to type "Restarting" in a task state:
this.store
@ -920,9 +923,9 @@ module('Acceptance | job status panel', function (hooks) {
await settled();
assert
.dom('.failed-or-lost-link')
.dom('.failed-or-lost-links > span')
.hasText(
'1',
'1 Restarted',
'Restarted cell updates when a task event with type "Restarting" is added'
);
});

View File

@ -9,6 +9,11 @@ module('Integration | Component | job-status/failed-or-lost', function (hooks) {
test('it renders', async function (assert) {
assert.expect(3);
let job = {
id: 'job1',
};
let allocs = [
{
id: 1,
@ -21,20 +26,23 @@ module('Integration | Component | job-status/failed-or-lost', function (hooks) {
];
this.set('allocs', allocs);
this.set('job', job);
await render(hbs`<JobStatus::FailedOrLost
@title="Rescheduled"
@description="Rescheduled Allocations"
@allocs={{this.allocs}}
@job={{this.job}}
@restartedAllocs={{this.allocs}}
/>`);
assert.dom('h4').hasText('Rescheduled');
assert.dom('.failed-or-lost-link').hasText('2');
assert.dom('h4').hasText('Replaced Allocations');
assert.dom('.failed-or-lost-links').hasText('2 Restarted');
await componentA11yAudit(this.element, assert);
});
test('it links or does not link appropriately', async function (assert) {
let job = {
id: 'job1',
};
let allocs = [
{
id: 1,
@ -47,16 +55,67 @@ module('Integration | Component | job-status/failed-or-lost', function (hooks) {
];
this.set('allocs', allocs);
this.set('job', job);
await render(hbs`<JobStatus::FailedOrLost
@title="Rescheduled"
@description="Rescheduled Allocations"
@allocs={{this.allocs}}
@restartedAllocs={{this.allocs}}
@job={{this.job}}
/>`);
// Ensure it's of type a
assert.dom('.failed-or-lost-link').hasTagName('a');
assert.dom('.failed-or-lost-links > span > *:last-child').hasTagName('a');
this.set('allocs', []);
assert.dom('.failed-or-lost-link').doesNotHaveTagName('a');
assert
.dom('.failed-or-lost-links > span > *:last-child')
.doesNotHaveTagName('a');
});
test('it shows rescheduling as well', async function (assert) {
let job = {
id: 'job1',
};
let restartedAllocs = [
{
id: 1,
name: 'alloc1',
},
{
id: 2,
name: 'alloc2',
},
];
let rescheduledAllocs = [
{
id: 1,
name: 'alloc1',
},
{
id: 2,
name: 'alloc2',
},
{
id: 3,
name: 'alloc3',
},
];
this.set('restartedAllocs', restartedAllocs);
this.set('rescheduledAllocs', rescheduledAllocs);
this.set('job', job);
this.set('supportsRescheduling', true);
await render(hbs`<JobStatus::FailedOrLost
@restartedAllocs={{this.restartedAllocs}}
@rescheduledAllocs={{this.rescheduledAllocs}}
@job={{this.job}}
@supportsRescheduling={{this.supportsRescheduling}}
/>`);
assert.dom('.failed-or-lost-links').containsText('2 Restarted');
assert.dom('.failed-or-lost-links').containsText('3 Rescheduled');
this.set('supportsRescheduling', false);
assert.dom('.failed-or-lost-links').doesNotContainText('Rescheduled');
});
});