[ui, feature] Job Page Redesign (#16932)
* [ui] Service job status panel (#16134) * it begins * Hacky demo enabled * Still very hacky but seems deece * Floor of at least 3 must be shown * Width from on-high * Other statuses considered * More sensible allocTypes listing * Beginnings of a legend * Total number of allocs running now maps over job.groups * Lintfix * base the number of slots to hold open on actual tallies, which should never exceed totalAllocs * Versions get yer versions here * Versions lookin like versions * Mirage fixup * Adds Remaining as an alloc chart status and adds historical status option * Get tests passing again by making job status static for a sec * Historical status panel click actions moved into their own component class * job detail tests plz chill * Testing if percy is fickle * Hyper-specfic on summary distribution bar identifier * Perhaps the 2nd allocSummary item no longer exists with the more accurate afterCreate data * UI Test eschewing the page pattern * Bones of a new acceptance test * Track width changes explicitly with window-resize * testlintfix * Alloc counting tests * Alloc grouping test * Alloc grouping with complex resizing * Refined the list of showable statuses * PR feedback addressed * renamed allocation-row to allocation-status-row * [ui, job status] Make panel status mode a queryParam (#16345) * queryParam changing * Test for QP in panel * Adding @tracked to legacy controller * Move the job of switching to Historical out to larger context * integration test mock passed func * [ui] Service job deployment status panel (#16383) * A very fast and loose deployment panel * Removing Unknown status from the panel * Set up oldAllocs list in constructor, rather than as a getter/tracked var * Small amount of template cleanup * Refactored latest-deployment new logic back into panel.js * Revert now-unused latest-deployment component * margin bottom when ungrouped also * Basic integration tests for job deployment status panel * Updates complete alloc colour to green for new visualizations only (#16618) * Updates complete alloc colour to green for new visualizations only * Pale green instead of dark green for viz in general * [ui] Job Deployment Status: History and Update Props (#16518) * Deployment history wooooooo * Styled deployment history * Update Params * lintfix * Types and groups for updateParams * Live-updating history * Harden with types, error states, and pending states * Refactor updateParams to use trigger component * [ui] Deployment History search (#16608) * Functioning searchbox * Some nice animations for history items * History search test * Fixing up some old mirage conventions * some a11y rule override to account for scss keyframes * Split panel into deploying and steady components * HandleError passed from job index * gridified panel elements * TotalAllocs added to deploying.js * Width perc to px * [ui] Splitting deployment allocs by status, health, and canary status (#16766) * Initial attempt with lots of scratchpad work * Style mods per UI discussion * Fix canary overflow bug * Dont show canary or health for steady/prev-alloc blocks * Steady state * Thanks Julie * Fixes steady-state versions * Legen, wait for it... * Test fixes now that we have a minimum block size * PR prep * Shimmer effect on pending and unplaced allocs (#16801) * Shimmer effect on pending and unplaced * Dont show animation in the legend * [ui, deployments] Linking allocblocks and legends to allocation / allocations index routes (#16821) * Conditional link-to component and basic linking to allocations and allocation routes * Job versions filter added to allocations index page * Steady state legends link * Legend links * Badge count links for versions * Fix: faded class on steady-state legend items * version link now wont show completed ones * Fix a11y violations with link labels * Combining some template conditional logic * [ui, deployments] Conversions on long nanosecond update params (#16882) * Conversions on long nanosecond nums * Early return in updateParamGroups comp prop * [ui, deployments] Mirage Actively Deploying Job and Deployment Integration Tests (#16888) * Start of deployment alloc test scaffolding * Bit of test cleanup and canary for ungrouped allocs * Flakey but more robust integrations for deployment panel * De-flake acceptance tests and add an actively deploying job to mirage * Jitter-less alloc status distribution removes my bad math * bugfix caused by summary.desiredTotal non-null * More interesting mirage active deployment alloc breakdown * Further tests for previous-allocs row * Previous alloc legend tests * Percy snapshots added to integration test * changelog
This commit is contained in:
parent
2c63d34296
commit
7dbebe9a93
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Job status and deployment redesign
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
{{#if @condition}}
|
||||
<LinkTo @route={{@route}} @model={{@model}} @query={{this.query}} class={{@class}} aria-label={{@label}}>
|
||||
{{yield}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<span class={{@class}}>
|
||||
{{yield}}
|
||||
</span>
|
||||
{{/if}}
|
|
@ -0,0 +1,7 @@
|
|||
import Component from '@glimmer/component';
|
||||
|
||||
export default class ConditionalLinkToComponent extends Component {
|
||||
get query() {
|
||||
return this.args.query || {};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// @ts-check
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { camelize } from '@ember/string';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class JobPagePartsSummaryChartComponent extends Component {
|
||||
@service router;
|
||||
|
||||
@action
|
||||
gotoAllocations(status) {
|
||||
this.router.transitionTo('jobs.job.allocations', this.args.job, {
|
||||
queryParams: {
|
||||
status: JSON.stringify(status),
|
||||
namespace: this.args.job.get('namespace.name'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onSliceClick(ev, slice) {
|
||||
this.gotoAllocations([camelize(slice.label)]);
|
||||
}
|
||||
}
|
|
@ -4,11 +4,10 @@
|
|||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
import { action, computed } from '@ember/object';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { classNames } from '@ember-decorators/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import { camelize } from '@ember/string';
|
||||
@classic
|
||||
@classNames('boxed-section')
|
||||
export default class Summary extends Component {
|
||||
|
@ -17,21 +16,6 @@ export default class Summary extends Component {
|
|||
job = null;
|
||||
forceCollapsed = false;
|
||||
|
||||
@action
|
||||
gotoAllocations(status) {
|
||||
this.router.transitionTo('jobs.job.allocations', this.job, {
|
||||
queryParams: {
|
||||
status: JSON.stringify(status),
|
||||
namespace: this.job.get('namespace.name'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onSliceClick(ev, slice) {
|
||||
this.gotoAllocations([camelize(slice.label)]);
|
||||
}
|
||||
|
||||
@computed('forceCollapsed')
|
||||
get isExpanded() {
|
||||
if (this.forceCollapsed) return false;
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<div
|
||||
class="allocation-status-block {{unless this.countToShow "rest-only"}}"
|
||||
style={{html-safe (concat "width: " @width "px")}}
|
||||
>
|
||||
{{#if this.countToShow}}
|
||||
<div class="ungrouped-allocs">
|
||||
{{#each (range 0 this.countToShow) as |i|}}
|
||||
<ConditionalLinkTo
|
||||
@condition={{not (eq @status "unplaced")}}
|
||||
@route="allocations.allocation"
|
||||
@model={{get @allocs i}}
|
||||
@class="represented-allocation {{@status}} {{@health}} {{@canary}}"
|
||||
@label="View allocation"
|
||||
>
|
||||
{{#if (and (eq @status "running") (not @steady))}}
|
||||
{{#if (eq @canary "canary")}}
|
||||
<span class="alloc-canary-indicator" />
|
||||
{{/if}}
|
||||
<span class="alloc-health-indicator">
|
||||
{{#if (eq @health "healthy")}}
|
||||
<FlightIcon @name="check" @color="white" />
|
||||
{{else}}
|
||||
<FlightIcon @name="running" @color="white" />
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</ConditionalLinkTo>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.remaining}}
|
||||
|
||||
<ConditionalLinkTo
|
||||
@condition={{not (eq @status "unplaced")}}
|
||||
@route="jobs.job.allocations"
|
||||
@model={{@allocs.0.job}}
|
||||
@query={{hash status=(concat '["' @status '"]') version=(concat '[' @allocs.0.jobVersion ']')}}
|
||||
@class="represented-allocation rest {{@status}} {{@health}} {{@canary}}"
|
||||
@label="View all {{@status}} allocations"
|
||||
>
|
||||
<span class="rest-count">{{#if this.countToShow}}+{{/if}}{{this.remaining}}</span>
|
||||
{{#unless @steady}}
|
||||
{{#if (eq @canary "canary")}}
|
||||
<span class="alloc-canary-indicator" />
|
||||
{{/if}}
|
||||
{{#if (eq @status "running")}}
|
||||
<span class="alloc-health-indicator">
|
||||
{{#if (eq @health "healthy")}}
|
||||
<FlightIcon @name="check" @color="#25ba81" />
|
||||
{{else}}
|
||||
<FlightIcon @name="running" @color="black" />
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</ConditionalLinkTo>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
import Component from '@glimmer/component';
|
||||
|
||||
export default class JobStatusAllocationStatusBlockComponent extends Component {
|
||||
get countToShow() {
|
||||
const restWidth = 50;
|
||||
const restGap = 10;
|
||||
let cts = Math.floor((this.args.width - (restWidth + restGap)) / 42);
|
||||
// Either show 3+ or show only a single/remaining box
|
||||
return cts > 3 ? cts : 0;
|
||||
}
|
||||
|
||||
get remaining() {
|
||||
return this.args.count - this.countToShow;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<div class="allocation-status-row">
|
||||
{{#if this.showSummaries}}
|
||||
<div class="alloc-status-summaries"
|
||||
{{did-insert this.captureElement}}
|
||||
{{window-resize this.reflow}}
|
||||
>
|
||||
{{#each-in @allocBlocks as |status allocsByStatus|}}
|
||||
{{#each-in allocsByStatus as |health allocsByHealth|}}
|
||||
{{#each-in allocsByHealth as |canary allocsByCanary|}}
|
||||
{{#if (gt allocsByCanary.length 0)}}
|
||||
<JobStatus::AllocationStatusBlock
|
||||
@status={{status}}
|
||||
@health={{health}}
|
||||
@canary={{canary}}
|
||||
@steady={{@steady}}
|
||||
@count={{allocsByCanary.length}}
|
||||
@width={{compute (action this.calcPerc) allocsByCanary.length}}
|
||||
@allocs={{allocsByCanary}} />
|
||||
{{/if}}
|
||||
{{/each-in}}
|
||||
{{/each-in}}
|
||||
{{/each-in}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="ungrouped-allocs"
|
||||
{{did-insert this.captureElement}}
|
||||
{{window-resize this.reflow}}
|
||||
>
|
||||
{{#each-in @allocBlocks as |status allocsByStatus|}}
|
||||
{{#each-in allocsByStatus as |health allocsByHealth|}}
|
||||
{{#each-in allocsByHealth as |canary allocsByCanary|}}
|
||||
{{#if (gt allocsByCanary.length 0)}}
|
||||
{{#each (range 0 allocsByCanary.length) as |i|}}
|
||||
<ConditionalLinkTo
|
||||
@condition={{not (eq status "unplaced")}}
|
||||
@route="allocations.allocation"
|
||||
@model={{get allocsByCanary i}}
|
||||
@class="represented-allocation {{status}} {{health}} {{canary}}"
|
||||
@label="View allocation"
|
||||
>
|
||||
{{#unless @steady}}
|
||||
{{#if (eq canary "canary")}}
|
||||
<span class="alloc-canary-indicator" />
|
||||
{{/if}}
|
||||
{{#if (eq status "running")}}
|
||||
<span class="alloc-health-indicator">
|
||||
{{#if (eq health "healthy")}}
|
||||
<FlightIcon @name="check" @color="white" />
|
||||
{{else}}
|
||||
<FlightIcon @name="running" @color="white" />
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</ConditionalLinkTo>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/each-in}}
|
||||
{{/each-in}}
|
||||
{{/each-in}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// @ts-check
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
const ALLOC_BLOCK_WIDTH = 32;
|
||||
const ALLOC_BLOCK_GAP = 10;
|
||||
|
||||
export default class JobStatusAllocationStatusRowComponent extends Component {
|
||||
@tracked width = 0;
|
||||
|
||||
get allocBlockSlots() {
|
||||
return Object.values(this.args.allocBlocks)
|
||||
.flatMap((statusObj) => Object.values(statusObj))
|
||||
.flatMap((healthObj) => Object.values(healthObj))
|
||||
.reduce(
|
||||
(totalSlots, allocsByCanary) =>
|
||||
totalSlots + (allocsByCanary ? allocsByCanary.length : 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
get showSummaries() {
|
||||
return (
|
||||
this.allocBlockSlots * (ALLOC_BLOCK_WIDTH + ALLOC_BLOCK_GAP) -
|
||||
ALLOC_BLOCK_GAP >
|
||||
this.width
|
||||
);
|
||||
}
|
||||
|
||||
calcPerc(count) {
|
||||
return (count / this.allocBlockSlots) * this.width;
|
||||
}
|
||||
|
||||
@action reflow(element) {
|
||||
this.width = element.clientWidth;
|
||||
}
|
||||
|
||||
@action
|
||||
captureElement(element) {
|
||||
this.width = element.clientWidth;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<div class="deployment-history">
|
||||
<header>
|
||||
<h4 class="title is-5">Deployment History</h4>
|
||||
<SearchBox
|
||||
data-test-history-search
|
||||
@searchTerm={{mut this.searchTerm}}
|
||||
@placeholder="Search events..."
|
||||
/>
|
||||
</header>
|
||||
<ol class="timeline">
|
||||
{{#each this.history as |deployment-log|}}
|
||||
<li class="timeline-object {{if (eq deployment-log.exitCode 1) "error"}}">
|
||||
<div class="boxed-section-head is-light">
|
||||
<LinkTo @route="allocations.allocation" @model={{deployment-log.state.allocation}} class="allocation-reference">{{deployment-log.state.allocation.shortId}}</LinkTo>
|
||||
<span><strong>{{deployment-log.type}}:</strong> {{deployment-log.message}}</span>
|
||||
<span class="pull-right">
|
||||
{{format-ts deployment-log.time}}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{{else}}
|
||||
{{#if this.errorState}}
|
||||
<li class="timeline-object">
|
||||
<div class="boxed-section-head is-light">
|
||||
<span>Error loading deployment history</span>
|
||||
</div>
|
||||
</li>
|
||||
{{else}}
|
||||
{{#if this.deploymentAllocations.length}}
|
||||
{{#if this.searchTerm}}
|
||||
<li class="timeline-object" data-test-history-search-no-match>
|
||||
<div class="boxed-section-head is-light">
|
||||
<span>No events match {{this.searchTerm}}</span>
|
||||
</div>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="timeline-object">
|
||||
<div class="boxed-section-head is-light">
|
||||
<span>No deployment events yet</span>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<li class="timeline-object">
|
||||
<div class="boxed-section-head is-light">
|
||||
<span>Loading deployment events</span>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
|
@ -0,0 +1,96 @@
|
|||
// @ts-check
|
||||
import Component from '@glimmer/component';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class JobStatusDeploymentHistoryComponent extends Component {
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* @type { Error }
|
||||
*/
|
||||
@tracked errorState = null;
|
||||
|
||||
/**
|
||||
* @type { import('../../models/job').default }
|
||||
*/
|
||||
@alias('args.deployment.job') job;
|
||||
|
||||
/**
|
||||
* @type { number }
|
||||
*/
|
||||
@alias('args.deployment.versionNumber') deploymentVersion;
|
||||
|
||||
/**
|
||||
* Get all allocations for the job
|
||||
* @type { import('../../models/allocation').default[] }
|
||||
*/
|
||||
get jobAllocations() {
|
||||
return this.job.get('allocations');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the job's allocations to only those that are part of the deployment
|
||||
* @type { import('../../models/allocation').default[] }
|
||||
*/
|
||||
get deploymentAllocations() {
|
||||
return this.jobAllocations.filter(
|
||||
(alloc) => alloc.jobVersion === this.deploymentVersion
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the deployment's allocations to their task events, in reverse-chronological order
|
||||
* @type { import('../../models/task-event').default[] }
|
||||
*/
|
||||
get history() {
|
||||
try {
|
||||
return this.deploymentAllocations
|
||||
.map((a) =>
|
||||
a
|
||||
.get('states')
|
||||
.map((s) => s.events.content)
|
||||
.flat()
|
||||
)
|
||||
.flat()
|
||||
.filter((a) => this.containsSearchTerm(a))
|
||||
.sort((a, b) => a.get('time') - b.get('time'))
|
||||
.reverse();
|
||||
} catch (e) {
|
||||
this.triggerError(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@action triggerError(error) {
|
||||
this.errorState = error;
|
||||
this.notifications.add({
|
||||
title: 'Could not fetch deployment history',
|
||||
message: error,
|
||||
color: 'critical',
|
||||
});
|
||||
}
|
||||
|
||||
// #region search
|
||||
|
||||
/**
|
||||
* @type { string }
|
||||
*/
|
||||
@tracked searchTerm = '';
|
||||
|
||||
/**
|
||||
* @param { import('../../models/task-event').default } taskEvent
|
||||
* @returns { boolean }
|
||||
*/
|
||||
containsSearchTerm(taskEvent) {
|
||||
return (
|
||||
taskEvent.message.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
||||
taskEvent.type.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
||||
taskEvent.state.allocation.shortId.includes(this.searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// #endregion search
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{{#if this.isActivelyDeploying}}
|
||||
<JobStatus::Panel::Deploying @job={{@job}} @handleError={{@handleError}} />
|
||||
{{else}}
|
||||
<JobStatus::Panel::Steady @job={{@job}} @statusMode={{@statusMode}} @setStatusMode={{@setStatusMode}} />
|
||||
{{/if}}
|
|
@ -0,0 +1,8 @@
|
|||
// @ts-check
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class JobStatusPanelComponent extends Component {
|
||||
get isActivelyDeploying() {
|
||||
return this.args.job.get('latestDeployment.isRunning');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
<div class="job-status-panel boxed-section active-deployment is-info" data-test-job-status-panel>
|
||||
<div class="boxed-section-head">
|
||||
<div class="boxed-section-row"
|
||||
{{did-insert (action this.establishOldAllocBlockIDs)}}
|
||||
>
|
||||
Deployment Status
|
||||
<span class="badge is-white bumper-left" data-test-active-deployment-stat="id">{{@job.latestDeployment.shortId}}</span>
|
||||
<div class="pull-right">
|
||||
{{#if @job.latestDeployment.isRunning}}
|
||||
<TwoStepButton
|
||||
data-test-fail
|
||||
@classes={{hash
|
||||
idleButton="is-danger"
|
||||
confirmationMessage="inherit-color"
|
||||
confirmButton="is-danger"}}
|
||||
@idleText="Fail Deployment"
|
||||
@cancelText="Cancel"
|
||||
@confirmText="Yes, Fail Deployment"
|
||||
@confirmationMessage="Are you sure?"
|
||||
@inlineText={{true}}
|
||||
@awaitingConfirmation={{this.fail.isRunning}}
|
||||
@disabled={{this.fail.isRunning}}
|
||||
@onConfirm={{perform this.fail}} />
|
||||
{{/if}}
|
||||
{{#if @job.latestDeployment.requiresPromotion}}
|
||||
<button
|
||||
data-test-promote-canary
|
||||
type="button"
|
||||
class="button is-warning is-small {{if this.promote.isRunning "is-loading"}}"
|
||||
disabled={{this.promote.isRunning}}
|
||||
onclick={{perform this.promote}}>Promote Canary</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
<div class="deployment-allocations">
|
||||
{{#if this.oldVersionAllocBlockIDs.length}}
|
||||
<h4 class="title is-5" data-test-old-allocation-tally>Previous allocations: {{#if this.oldVersionAllocBlocks.running}}{{this.oldRunningHealthyAllocBlocks.length}} running{{/if}}</h4>
|
||||
<div class="previous-allocations">
|
||||
<JobStatus::AllocationStatusRow @allocBlocks={{this.oldVersionAllocBlocks}} @steady={{true}} />
|
||||
</div>
|
||||
<div class="legend-and-summary" data-test-previous-allocations-legend>
|
||||
<legend>
|
||||
<span class="legend-item {{if (eq (get this.oldRunningHealthyAllocBlocks "length") 0) "faded"}}">
|
||||
<span class="represented-allocation running"></span>
|
||||
{{get this.oldRunningHealthyAllocBlocks "length"}} Running
|
||||
</span>
|
||||
<span class="legend-item {{if (eq (get this.oldCompleteHealthyAllocBlocks "length") 0) "faded"}}">
|
||||
<span class="represented-allocation complete"></span>
|
||||
{{get this.oldCompleteHealthyAllocBlocks "length"}} Complete
|
||||
</span>
|
||||
</legend>
|
||||
</div>
|
||||
|
||||
{{/if}}
|
||||
|
||||
<h4 class="title is-5" data-test-new-allocation-tally>New allocations: {{this.newRunningHealthyAllocBlocks.length}}/{{this.totalAllocs}} running and healthy</h4>
|
||||
<div class="new-allocations">
|
||||
<JobStatus::AllocationStatusRow @allocBlocks={{this.newVersionAllocBlocks}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend-and-summary" data-test-new-allocations-legend>
|
||||
|
||||
{{!-- Legend by Status, then by Health, then by Canary --}}
|
||||
<legend>
|
||||
{{#each-in this.newAllocsByStatus as |status count|}}
|
||||
<ConditionalLinkTo
|
||||
@condition={{not (eq status "unplaced")}}
|
||||
@route="jobs.job.allocations"
|
||||
@model={{@job}}
|
||||
@query={{hash status=(concat '["' status '"]') version=(concat '[' this.job.latestDeployment.versionNumber ']')}}
|
||||
@class="legend-item {{if (eq count 0) "faded"}}"
|
||||
@label="View {{status}} allocations"
|
||||
>
|
||||
<span class="represented-allocation {{status}}"></span>
|
||||
{{count}} {{capitalize status}}
|
||||
</ConditionalLinkTo>
|
||||
{{/each-in}}
|
||||
|
||||
{{#each-in this.newAllocsByHealth as |health count|}}
|
||||
<span class="legend-item {{if (eq count 0) "faded"}}">
|
||||
<span class="represented-allocation legend-example">
|
||||
<span class="alloc-health-indicator">
|
||||
{{#if (eq health "healthy")}}
|
||||
<FlightIcon @name="check" @color="#25ba81" />
|
||||
{{else}}
|
||||
<FlightIcon @name="running" @color="black" class="not-animated" />
|
||||
{{/if}}
|
||||
</span>
|
||||
</span>
|
||||
{{count}} {{capitalize health}}
|
||||
</span>
|
||||
{{/each-in}}
|
||||
|
||||
<span class="legend-item {{if (eq this.newAllocsByCanary.canary 0) "faded"}}">
|
||||
<span class="represented-allocation legend-example canary">
|
||||
<span class="alloc-canary-indicator" />
|
||||
</span>
|
||||
{{this.newAllocsByCanary.canary}} Canary
|
||||
</span>
|
||||
|
||||
</legend>
|
||||
</div>
|
||||
|
||||
<div class="history-and-params">
|
||||
<JobStatus::DeploymentHistory @deployment={{@job.latestDeployment}} />
|
||||
<JobStatus::UpdateParams @job={{@job}} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,185 @@
|
|||
// @ts-check
|
||||
import Component from '@glimmer/component';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
|
||||
|
||||
export default class JobStatusPanelDeployingComponent extends Component {
|
||||
@alias('args.job') job;
|
||||
@alias('args.handleError') handleError = () => {};
|
||||
|
||||
allocTypes = [
|
||||
'running',
|
||||
'pending',
|
||||
'failed',
|
||||
// 'unknown',
|
||||
// 'lost',
|
||||
// 'queued',
|
||||
// 'complete',
|
||||
'unplaced',
|
||||
].map((type) => {
|
||||
return {
|
||||
label: type,
|
||||
};
|
||||
});
|
||||
|
||||
@tracked oldVersionAllocBlockIDs = [];
|
||||
|
||||
// Called via did-insert; sets a static array of "outgoing"
|
||||
// allocations we can track throughout a deployment
|
||||
establishOldAllocBlockIDs() {
|
||||
this.oldVersionAllocBlockIDs = this.job.allocations.filter(
|
||||
(a) =>
|
||||
a.clientStatus === 'running' &&
|
||||
a.jobVersion !== this.deployment.get('versionNumber')
|
||||
);
|
||||
}
|
||||
|
||||
@task(function* () {
|
||||
try {
|
||||
yield this.job.latestDeployment.content.promote();
|
||||
} catch (err) {
|
||||
this.handleError({
|
||||
title: 'Could Not Promote Deployment',
|
||||
description: messageFromAdapterError(err, 'promote deployments'),
|
||||
});
|
||||
}
|
||||
})
|
||||
promote;
|
||||
|
||||
@task(function* () {
|
||||
try {
|
||||
yield this.job.latestDeployment.content.fail();
|
||||
} catch (err) {
|
||||
this.handleError({
|
||||
title: 'Could Not Fail Deployment',
|
||||
description: messageFromAdapterError(err, 'fail deployments'),
|
||||
});
|
||||
}
|
||||
})
|
||||
fail;
|
||||
|
||||
@alias('job.latestDeployment') deployment;
|
||||
@alias('deployment.desiredTotal') desiredTotal;
|
||||
|
||||
get oldVersionAllocBlocks() {
|
||||
return this.job.allocations
|
||||
.filter((allocation) => this.oldVersionAllocBlockIDs.includes(allocation))
|
||||
.reduce((alloGroups, currentAlloc) => {
|
||||
const status = currentAlloc.clientStatus;
|
||||
|
||||
if (!alloGroups[status]) {
|
||||
alloGroups[status] = {
|
||||
healthy: { nonCanary: [] },
|
||||
unhealthy: { nonCanary: [] },
|
||||
};
|
||||
}
|
||||
alloGroups[status].healthy.nonCanary.push(currentAlloc);
|
||||
|
||||
return alloGroups;
|
||||
}, {});
|
||||
}
|
||||
|
||||
get newVersionAllocBlocks() {
|
||||
let availableSlotsToFill = this.desiredTotal;
|
||||
let allocationsOfDeploymentVersion = this.job.allocations.filter(
|
||||
(a) => a.jobVersion === this.deployment.get('versionNumber')
|
||||
);
|
||||
|
||||
let allocationCategories = this.allocTypes.reduce((categories, type) => {
|
||||
categories[type.label] = {
|
||||
healthy: { canary: [], nonCanary: [] },
|
||||
unhealthy: { canary: [], nonCanary: [] },
|
||||
};
|
||||
return categories;
|
||||
}, {});
|
||||
|
||||
for (let alloc of allocationsOfDeploymentVersion) {
|
||||
if (availableSlotsToFill <= 0) {
|
||||
break;
|
||||
}
|
||||
let status = alloc.clientStatus;
|
||||
let health = alloc.isHealthy ? 'healthy' : 'unhealthy';
|
||||
let canary = alloc.isCanary ? 'canary' : 'nonCanary';
|
||||
|
||||
if (allocationCategories[status]) {
|
||||
allocationCategories[status][health][canary].push(alloc);
|
||||
availableSlotsToFill--;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill unplaced slots if availableSlotsToFill > 0
|
||||
if (availableSlotsToFill > 0) {
|
||||
allocationCategories['unplaced'] = {
|
||||
healthy: { canary: [], nonCanary: [] },
|
||||
unhealthy: { canary: [], nonCanary: [] },
|
||||
};
|
||||
allocationCategories['unplaced']['healthy']['nonCanary'] = Array(
|
||||
availableSlotsToFill
|
||||
)
|
||||
.fill()
|
||||
.map(() => {
|
||||
return { clientStatus: 'unplaced' };
|
||||
});
|
||||
}
|
||||
|
||||
return allocationCategories;
|
||||
}
|
||||
|
||||
get newRunningHealthyAllocBlocks() {
|
||||
return [
|
||||
...this.newVersionAllocBlocks['running']['healthy']['canary'],
|
||||
...this.newVersionAllocBlocks['running']['healthy']['nonCanary'],
|
||||
];
|
||||
}
|
||||
|
||||
// #region legend
|
||||
get newAllocsByStatus() {
|
||||
return Object.entries(this.newVersionAllocBlocks).reduce(
|
||||
(counts, [status, healthStatusObj]) => {
|
||||
counts[status] = Object.values(healthStatusObj)
|
||||
.flatMap((canaryStatusObj) => Object.values(canaryStatusObj))
|
||||
.flatMap((canaryStatusArray) => canaryStatusArray).length;
|
||||
return counts;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
get newAllocsByCanary() {
|
||||
return Object.values(this.newVersionAllocBlocks)
|
||||
.flatMap((healthStatusObj) => Object.values(healthStatusObj))
|
||||
.flatMap((canaryStatusObj) => Object.entries(canaryStatusObj))
|
||||
.reduce((counts, [canaryStatus, items]) => {
|
||||
counts[canaryStatus] = (counts[canaryStatus] || 0) + items.length;
|
||||
return counts;
|
||||
}, {});
|
||||
}
|
||||
|
||||
get newAllocsByHealth() {
|
||||
return {
|
||||
healthy: this.newRunningHealthyAllocBlocks.length,
|
||||
'health unknown':
|
||||
this.totalAllocs - this.newRunningHealthyAllocBlocks.length,
|
||||
};
|
||||
}
|
||||
// #endregion legend
|
||||
|
||||
get oldRunningHealthyAllocBlocks() {
|
||||
return this.oldVersionAllocBlocks.running?.healthy?.nonCanary || [];
|
||||
}
|
||||
get oldCompleteHealthyAllocBlocks() {
|
||||
return this.oldVersionAllocBlocks.complete?.healthy?.nonCanary || [];
|
||||
}
|
||||
|
||||
// TODO: eventually we will want this from a new property on a job.
|
||||
// TODO: consolidate w/ the one in steady.js
|
||||
get totalAllocs() {
|
||||
// v----- Experimental method: Count all allocs. Good for testing but not a realistic representation of "Desired"
|
||||
// return this.allocTypes.reduce((sum, type) => sum + this.args.job[type.property], 0);
|
||||
|
||||
// v----- Realistic method: Tally a job's task groups' "count" property
|
||||
return this.args.job.taskGroups.reduce((sum, tg) => sum + tg.count, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<div class="job-status-panel boxed-section steady-state {{if (eq @statusMode "historical") "historical-state" "current-state"}}" data-test-job-status-panel data-test-status-mode={{@statusMode}}>
|
||||
<div class="boxed-section-head">
|
||||
<h2>Status</h2>
|
||||
<div class="select-mode">
|
||||
<button type="button"
|
||||
data-test-status-mode-current
|
||||
class="button is-small is-borderless {{if (eq @statusMode "current") "is-active"}}"
|
||||
{{on "click" (action (optional @setStatusMode) "current")}}
|
||||
>
|
||||
Current
|
||||
</button>
|
||||
<button type="button"
|
||||
data-test-status-mode-historical
|
||||
class="button is-small is-borderless {{if (eq @statusMode "historical") "is-active"}}"
|
||||
{{on "click" (action (optional @setStatusMode) "historical")}}>
|
||||
Historical
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
{{#if (eq @statusMode "historical")}}
|
||||
<JobPage::Parts::SummaryChart @job={{@job}} />
|
||||
{{else}}
|
||||
<h3 class="title is-4 running-allocs-title"><strong>{{@job.runningAllocs}}/{{this.totalAllocs}}</strong> Allocations Running</h3>
|
||||
<JobStatus::AllocationStatusRow @allocBlocks={{this.allocBlocks}} @steady={{true}} />
|
||||
|
||||
<div class="legend-and-summary">
|
||||
<legend>
|
||||
{{#each this.allocTypes as |type|}}
|
||||
<ConditionalLinkTo
|
||||
@condition={{not (eq type.label "unplaced")}}
|
||||
@route="jobs.job.allocations"
|
||||
@model={{@job}}
|
||||
@query={{hash status=(concat '["' type.label '"]') version=(concat '[' (keys this.versions) ']')}}
|
||||
@class="legend-item {{if (eq (get (get (get (get this.allocBlocks type.label) 'healthy') 'nonCanary') "length") 0) "faded"}}"
|
||||
@label="View {{type.label}} allocations"
|
||||
>
|
||||
<span class="represented-allocation {{type.label}}"></span>
|
||||
{{get (get (get (get this.allocBlocks type.label) 'healthy') 'nonCanary') "length"}} {{capitalize type.label}}
|
||||
</ConditionalLinkTo>
|
||||
{{/each}}
|
||||
</legend>
|
||||
|
||||
<section class="versions">
|
||||
<h4>Versions</h4>
|
||||
<ul>
|
||||
{{#each-in this.versions as |version allocs|}}
|
||||
<li>
|
||||
<LinkTo @route="jobs.job.allocations" @model={{@job}} @query={{hash version=(concat '[' version ']') status=(concat '["running", "pending", "failed"]') }}>
|
||||
<Hds::Badge @text={{concat "v" version}} />
|
||||
<Hds::BadgeCount @text={{allocs.length}} @type="inverted" />
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/each-in}}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,87 @@
|
|||
// @ts-check
|
||||
import Component from '@glimmer/component';
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
export default class JobStatusPanelSteadyComponent extends Component {
|
||||
@alias('args.job') job;
|
||||
|
||||
// Build note: allocTypes order matters! We will fill up to 100% of totalAllocs in this order.
|
||||
allocTypes = [
|
||||
'running',
|
||||
'pending',
|
||||
'failed',
|
||||
// 'unknown',
|
||||
// 'lost',
|
||||
// 'queued',
|
||||
// 'complete',
|
||||
'unplaced',
|
||||
].map((type) => {
|
||||
return {
|
||||
label: type,
|
||||
};
|
||||
});
|
||||
|
||||
get allocBlocks() {
|
||||
let availableSlotsToFill = this.totalAllocs;
|
||||
// Only fill up to 100% of totalAllocs. Once we've filled up, we can stop counting.
|
||||
let allocationsOfShowableType = this.allocTypes.reduce((blocks, type) => {
|
||||
const jobAllocsOfType = this.args.job.allocations.filterBy(
|
||||
'clientStatus',
|
||||
type.label
|
||||
);
|
||||
if (availableSlotsToFill > 0) {
|
||||
blocks[type.label] = {
|
||||
healthy: {
|
||||
nonCanary: Array(
|
||||
Math.min(availableSlotsToFill, jobAllocsOfType.length)
|
||||
)
|
||||
.fill()
|
||||
.map((_, i) => {
|
||||
return jobAllocsOfType[i];
|
||||
}),
|
||||
},
|
||||
};
|
||||
availableSlotsToFill -= blocks[type.label].healthy.nonCanary.length;
|
||||
} else {
|
||||
blocks[type.label] = { healthy: { nonCanary: [] } };
|
||||
}
|
||||
return blocks;
|
||||
}, {});
|
||||
if (availableSlotsToFill > 0) {
|
||||
allocationsOfShowableType['unplaced'] = {
|
||||
healthy: {
|
||||
nonCanary: Array(availableSlotsToFill)
|
||||
.fill()
|
||||
.map(() => {
|
||||
return { clientStatus: 'unplaced' };
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return allocationsOfShowableType;
|
||||
}
|
||||
|
||||
// TODO: eventually we will want this from a new property on a job.
|
||||
get totalAllocs() {
|
||||
// v----- Experimental method: Count all allocs. Good for testing but not a realistic representation of "Desired"
|
||||
// return this.allocTypes.reduce((sum, type) => sum + this.args.job[type.property], 0);
|
||||
|
||||
// v----- Realistic method: Tally a job's task groups' "count" property
|
||||
return this.args.job.taskGroups.reduce((sum, tg) => sum + tg.count, 0);
|
||||
}
|
||||
|
||||
get versions() {
|
||||
return Object.values(this.allocBlocks)
|
||||
.flatMap((allocType) => Object.values(allocType))
|
||||
.flatMap((allocHealth) => Object.values(allocHealth))
|
||||
.flatMap((allocCanary) => Object.values(allocCanary))
|
||||
.map((a) => (!isNaN(a?.jobVersion) ? a.jobVersion : 'pending')) // "starting" allocs, and possibly others, do not yet have a jobVersion
|
||||
.reduce(
|
||||
(result, item) => ({
|
||||
...result,
|
||||
[item]: [...(result[item] || []), item],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<Trigger @onError={{action this.onError}} @do={{this.fetchJobDefinition}} as |trigger|>
|
||||
{{did-insert trigger.fns.do}}
|
||||
|
||||
<div class="update-parameters">
|
||||
<h4 class="title is-5">Update Params</h4>
|
||||
<code>
|
||||
|
||||
{{#if (and trigger.data.isSuccess (not trigger.data.isError))}}
|
||||
<ul>
|
||||
{{#each this.updateParamGroups as |group|}}
|
||||
<li>
|
||||
<span class="group">Group "{{group.name}}"</span>
|
||||
<ul>
|
||||
{{#each-in group.update as |k v|}}
|
||||
<li>
|
||||
<span class="key">{{k}}</span>
|
||||
<span class="value">{{v}}</span>
|
||||
</li>
|
||||
{{/each-in}}
|
||||
</ul>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#if trigger.data.isBusy}}
|
||||
<span class="notification">Loading Parameters</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if trigger.data.isError}}
|
||||
<span class="notification">Error loading parameters</span>
|
||||
{{/if}}
|
||||
|
||||
</code>
|
||||
</div>
|
||||
</Trigger>
|
|
@ -0,0 +1,77 @@
|
|||
// @ts-check
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import formatDuration from 'nomad-ui/utils/format-duration';
|
||||
|
||||
/**
|
||||
* @typedef {Object} DefinitionUpdateStrategy
|
||||
* @property {boolean} AutoPromote
|
||||
* @property {boolean} AutoRevert
|
||||
* @property {number} Canary
|
||||
* @property {number} MaxParallel
|
||||
* @property {string} HealthCheck
|
||||
* @property {number} MinHealthyTime
|
||||
* @property {number} HealthyDeadline
|
||||
* @property {number} ProgressDeadline
|
||||
* @property {number} Stagger
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DefinitionTaskGroup
|
||||
* @property {string} Name
|
||||
* @property {number} Count
|
||||
* @property {DefinitionUpdateStrategy} Update
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} JobDefinition
|
||||
* @property {string} ID
|
||||
* @property {DefinitionUpdateStrategy} Update
|
||||
* @property {DefinitionTaskGroup[]} TaskGroups
|
||||
*/
|
||||
|
||||
const PARAMS_REQUIRING_CONVERSION = [
|
||||
'HealthyDeadline',
|
||||
'MinHealthyTime',
|
||||
'ProgressDeadline',
|
||||
'Stagger',
|
||||
];
|
||||
|
||||
export default class JobStatusUpdateParamsComponent extends Component {
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* @type {JobDefinition}
|
||||
*/
|
||||
@tracked rawDefinition = null;
|
||||
|
||||
get updateParamGroups() {
|
||||
if (!this.rawDefinition) {
|
||||
return null;
|
||||
}
|
||||
return this.rawDefinition.TaskGroups.map((tg) => ({
|
||||
name: tg.Name,
|
||||
update: Object.keys(tg.Update || {}).reduce((newUpdateObj, key) => {
|
||||
newUpdateObj[key] = PARAMS_REQUIRING_CONVERSION.includes(key)
|
||||
? formatDuration(tg.Update[key])
|
||||
: tg.Update[key];
|
||||
return newUpdateObj;
|
||||
}, {}),
|
||||
}));
|
||||
}
|
||||
|
||||
@action onError({ Error }) {
|
||||
const error = Error.errors[0].title || 'Error fetching job parameters';
|
||||
this.notifications.add({
|
||||
title: 'Could not fetch job definition',
|
||||
message: error,
|
||||
color: 'critical',
|
||||
});
|
||||
}
|
||||
|
||||
@action async fetchJobDefinition() {
|
||||
this.rawDefinition = await this.args.job.fetchRawDefinition();
|
||||
}
|
||||
}
|
|
@ -46,12 +46,16 @@ export default class AllocationsController extends Controller.extend(
|
|||
{
|
||||
qpTaskGroup: 'taskGroup',
|
||||
},
|
||||
{
|
||||
qpVersion: 'version',
|
||||
},
|
||||
'activeTask',
|
||||
];
|
||||
|
||||
qpStatus = '';
|
||||
qpClient = '';
|
||||
qpTaskGroup = '';
|
||||
qpVersion = '';
|
||||
currentPage = 1;
|
||||
pageSize = 25;
|
||||
activeTask = null;
|
||||
|
@ -75,10 +79,16 @@ export default class AllocationsController extends Controller.extend(
|
|||
'allocations.[]',
|
||||
'selectionStatus',
|
||||
'selectionClient',
|
||||
'selectionTaskGroup'
|
||||
'selectionTaskGroup',
|
||||
'selectionVersion'
|
||||
)
|
||||
get filteredAllocations() {
|
||||
const { selectionStatus, selectionClient, selectionTaskGroup } = this;
|
||||
const {
|
||||
selectionStatus,
|
||||
selectionClient,
|
||||
selectionTaskGroup,
|
||||
selectionVersion,
|
||||
} = this;
|
||||
|
||||
return this.allocations.filter((alloc) => {
|
||||
if (
|
||||
|
@ -99,6 +109,12 @@ export default class AllocationsController extends Controller.extend(
|
|||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
selectionVersion.length &&
|
||||
!selectionVersion.includes(alloc.jobVersion)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
@ -110,6 +126,7 @@ export default class AllocationsController extends Controller.extend(
|
|||
@selection('qpStatus') selectionStatus;
|
||||
@selection('qpClient') selectionClient;
|
||||
@selection('qpTaskGroup') selectionTaskGroup;
|
||||
@selection('qpVersion') selectionVersion;
|
||||
|
||||
@action
|
||||
gotoAllocation(allocation) {
|
||||
|
@ -163,6 +180,24 @@ export default class AllocationsController extends Controller.extend(
|
|||
return taskGroups.sort().map((tg) => ({ key: tg, label: tg }));
|
||||
}
|
||||
|
||||
@computed('model.allocations.[]', 'selectionVersion')
|
||||
get optionsVersions() {
|
||||
const versions = Array.from(
|
||||
new Set(this.model.allocations.mapBy('jobVersion'))
|
||||
).compact();
|
||||
|
||||
// Update query param when the list of versions changes.
|
||||
scheduleOnce('actions', () => {
|
||||
// eslint-disable-next-line ember/no-side-effects
|
||||
this.set(
|
||||
'qpVersion',
|
||||
serialize(intersection(versions, this.selectionVersion))
|
||||
);
|
||||
});
|
||||
|
||||
return versions.sort((a, b) => a - b).map((v) => ({ key: v, label: v }));
|
||||
}
|
||||
|
||||
setFacetQueryParam(queryParam, selection) {
|
||||
this.set(queryParam, serialize(selection));
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
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 classic from 'ember-classic-decorator';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
@classic
|
||||
export default class IndexController extends Controller.extend(
|
||||
WithNamespaceResetting
|
||||
|
@ -26,6 +28,7 @@ export default class IndexController extends Controller.extend(
|
|||
sortDescending: 'desc',
|
||||
},
|
||||
'activeTask',
|
||||
'statusMode',
|
||||
];
|
||||
|
||||
currentPage = 1;
|
||||
|
@ -34,14 +37,29 @@ export default class IndexController extends Controller.extend(
|
|||
|
||||
sortProperty = 'name';
|
||||
sortDescending = false;
|
||||
activeTask = null;
|
||||
|
||||
@tracked activeTask = null;
|
||||
|
||||
/**
|
||||
* @type {('current'|'historical')}
|
||||
*/
|
||||
@tracked
|
||||
statusMode = 'current';
|
||||
|
||||
@action
|
||||
setActiveTaskQueryParam(task) {
|
||||
if (task) {
|
||||
this.set('activeTask', `${task.allocation.id}-${task.name}`);
|
||||
this.activeTask = `${task.allocation.id}-${task.name}`;
|
||||
} else {
|
||||
this.set('activeTask', null);
|
||||
this.activeTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {('current'|'historical')} mode
|
||||
*/
|
||||
@action
|
||||
setStatusMode(mode) {
|
||||
this.statusMode = mode;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,15 @@ export default class Allocation extends Model {
|
|||
|
||||
@attr('string') clientStatus;
|
||||
@attr('string') desiredStatus;
|
||||
@attr() deploymentStatus;
|
||||
|
||||
get isCanary() {
|
||||
return this.deploymentStatus?.Canary;
|
||||
}
|
||||
|
||||
get isHealthy() {
|
||||
return this.deploymentStatus?.Healthy;
|
||||
}
|
||||
|
||||
@attr healthChecks;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
$queued: $grey-lighter;
|
||||
$starting: $grey-lighter;
|
||||
$running: $primary;
|
||||
$complete: $nomad-green-dark;
|
||||
$complete: $nomad-green-pale;
|
||||
$failed: $danger;
|
||||
$lost: $dark;
|
||||
$not-scheduled: $blue-200;
|
||||
|
|
|
@ -58,3 +58,4 @@
|
|||
@import './components/authorization';
|
||||
@import './components/policies';
|
||||
@import './components/metadata-editor';
|
||||
@import './components/job-status-panel';
|
||||
|
|
|
@ -0,0 +1,442 @@
|
|||
.job-status-panel {
|
||||
// #region layout
|
||||
&.steady-state.current-state .boxed-section-body {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'title'
|
||||
'allocation-status-row'
|
||||
'legend-and-summary';
|
||||
gap: 1rem;
|
||||
grid-auto-columns: 100%;
|
||||
|
||||
& > h3 {
|
||||
grid-area: title;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > .allocation-status-row {
|
||||
grid-area: allocation-status-row;
|
||||
}
|
||||
}
|
||||
|
||||
&.active-deployment .boxed-section-body {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'deployment-allocations'
|
||||
'legend-and-summary'
|
||||
'history-and-params';
|
||||
gap: 1rem;
|
||||
grid-auto-columns: 100%;
|
||||
|
||||
& > .deployment-allocations {
|
||||
grid-area: deployment-allocations;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-auto-columns: 100%;
|
||||
|
||||
& > h4 {
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
& > .history-and-params {
|
||||
grid-area: history-and-params;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-and-summary {
|
||||
// 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: 50% 50%;
|
||||
|
||||
legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.versions {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
& > ul {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 100px);
|
||||
gap: 0.5rem;
|
||||
& > li {
|
||||
white-space: nowrap;
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion layout
|
||||
|
||||
.select-mode {
|
||||
border: 1px solid $grey-blue;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 2px;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin-left: 1rem;
|
||||
|
||||
button {
|
||||
height: auto;
|
||||
padding: 0 0.5rem;
|
||||
background: transparent;
|
||||
transition: 0.1s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.running-allocs-title {
|
||||
strong {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.ungrouped-allocs {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 32px;
|
||||
|
||||
& > .represented-allocation {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.alloc-status-summaries {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
gap: 1.5rem;
|
||||
|
||||
.allocation-status-block {
|
||||
display: grid;
|
||||
grid-template-columns: auto 50px;
|
||||
gap: 10px;
|
||||
|
||||
&.rest-only {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
& > .ungrouped-allocs {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 10px;
|
||||
grid-auto-columns: unset;
|
||||
& > .represented-allocation {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.represented-allocation.rest {
|
||||
// TODO: we eventually want to establish a minimum width here. However, we need to also include this in the allocation-status-block width computation.
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
|
||||
& > .rest-count {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.unplaced {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.represented-allocation {
|
||||
background: $green;
|
||||
border-radius: 4px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
color: white;
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
$queued: $grey;
|
||||
$pending: $grey-lighter;
|
||||
$running: $primary;
|
||||
$complete: $nomad-green-pale;
|
||||
$failed: $danger;
|
||||
$lost: $dark;
|
||||
|
||||
// Client Statuses
|
||||
&.running {
|
||||
background: $running;
|
||||
}
|
||||
&.failed {
|
||||
background: $failed;
|
||||
}
|
||||
&.unknown {
|
||||
background: $unknown;
|
||||
}
|
||||
&.queued {
|
||||
background: $queued;
|
||||
}
|
||||
&.complete {
|
||||
background: $complete;
|
||||
color: black;
|
||||
}
|
||||
&.pending {
|
||||
background: $pending;
|
||||
color: black;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(-60deg, $pending, #eee, $pending);
|
||||
animation: shimmer 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
&.lost {
|
||||
background: $lost;
|
||||
}
|
||||
|
||||
&.unplaced {
|
||||
background: $grey-lighter;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:before {
|
||||
background: linear-gradient(-60deg, $pending, #eee, $pending);
|
||||
animation: shimmer 2s ease-in-out infinite;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
margin: 2px;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.legend-example {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
// Health Statuses
|
||||
|
||||
.alloc-health-indicator {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.running {
|
||||
.alloc-health-indicator {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
&.rest .alloc-health-indicator {
|
||||
top: -7px;
|
||||
right: -7px;
|
||||
border-radius: 20px;
|
||||
background: white;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.5);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
transform: scale(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
// Canary Status
|
||||
&.canary > .alloc-canary-indicator {
|
||||
overflow: hidden;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
bottom: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transform: rotate(45deg);
|
||||
background-color: $orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legend-item .represented-allocation .flight-icon {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
& > .boxed-section-body > .deployment-allocations {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: auto 1fr;
|
||||
|
||||
&.faded {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.represented-allocation {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: none;
|
||||
&:before,
|
||||
&:after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-and-params {
|
||||
display: grid;
|
||||
grid-template-columns: 70% auto;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.deployment-history {
|
||||
& > header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: end;
|
||||
& > .search-box {
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
& > ol {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
& > ol > li {
|
||||
@for $i from 1 through 50 {
|
||||
&:nth-child(#{$i}) {
|
||||
animation-name: historyItemSlide;
|
||||
animation-duration: 0.2s;
|
||||
animation-fill-mode: both;
|
||||
animation-delay: 0.1s + (0.05 * $i);
|
||||
}
|
||||
|
||||
&:nth-child(#{$i}) > div {
|
||||
animation-name: historyItemShine;
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: both;
|
||||
animation-delay: 0.1s + (0.05 * $i);
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
&.error > div {
|
||||
border: 1px solid $danger;
|
||||
background: rgba($danger, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.update-parameters {
|
||||
& > code {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
display: block;
|
||||
}
|
||||
ul,
|
||||
span.notification {
|
||||
display: block;
|
||||
background: #1a2633;
|
||||
padding: 1rem;
|
||||
color: white;
|
||||
.key {
|
||||
color: #1caeff;
|
||||
&:after {
|
||||
content: '=';
|
||||
color: white;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
.value {
|
||||
color: #06d092;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes historyItemSlide {
|
||||
from {
|
||||
opacity: 0;
|
||||
top: -40px;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes historyItemShine {
|
||||
from {
|
||||
box-shadow: inset 0 0 0 100px rgba(255, 200, 0, 0.2);
|
||||
}
|
||||
to {
|
||||
box-shadow: inset 0 0 0 100px rgba(255, 200, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
}
|
||||
30% {
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
}
|
|
@ -30,8 +30,8 @@
|
|||
}
|
||||
|
||||
&.is-complete {
|
||||
background: $nomad-green-dark;
|
||||
color: findColorInvert($nomad-green-dark);
|
||||
background: $nomad-green-pale;
|
||||
color: findColorInvert($nomad-green-pale);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
|
@ -87,13 +87,12 @@
|
|||
width: 1rem;
|
||||
}
|
||||
|
||||
|
||||
$tagPadding: 0.75em;
|
||||
|
||||
&.canary {
|
||||
overflow: hidden;
|
||||
&:before {
|
||||
content: "Canary";
|
||||
content: 'Canary';
|
||||
background-color: $blue-light;
|
||||
color: $black;
|
||||
line-height: 1.5em;
|
||||
|
|
|
@ -19,5 +19,6 @@ $vagrant-blue-dark: #104eb2;
|
|||
$nomad-green: #25ba81;
|
||||
$nomad-green-dark: #1d9467;
|
||||
$nomad-green-darker: #16704d;
|
||||
$nomad-green-pale: #d9f0e6;
|
||||
|
||||
$serf-red: #dd4e58;
|
||||
|
|
|
@ -33,6 +33,12 @@
|
|||
"job-page/parts/job-client-status-summary" job=@job
|
||||
)
|
||||
Children=(component "job-page/parts/children" job=@job)
|
||||
|
||||
StatusPanel=(component
|
||||
"job-status/panel" job=@job
|
||||
handleError=this.handleError
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
}}
|
|
@ -0,0 +1,62 @@
|
|||
{{#if @job.hasChildren}}
|
||||
<ChildrenStatusBar
|
||||
@allocationContainer={{@job.summary}}
|
||||
@job={{@job.summary}}
|
||||
@class="split-view" as |chart|
|
||||
>
|
||||
<ol data-test-legend class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li
|
||||
class="{{datum.className}}
|
||||
|
||||
{{if (eq datum.label chart.activeDatum.label) "is-active"}}
|
||||
|
||||
{{if (eq datum.value 0) "is-empty"}}"
|
||||
>
|
||||
<JobPage::Parts::SummaryLegendItem
|
||||
@datum={{datum}}
|
||||
@index={{index}}
|
||||
/>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</ChildrenStatusBar>
|
||||
{{else}}
|
||||
<AllocationStatusBar
|
||||
@allocationContainer={{@job.summary}}
|
||||
@job={{@job}}
|
||||
@onSliceClick={{this.onSliceClick}}
|
||||
@class="split-view" as |chart|
|
||||
>
|
||||
<ol data-test-legend class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li
|
||||
data-test-legend-label="{{datum.className}}"
|
||||
class="{{datum.className}}
|
||||
|
||||
{{if (eq datum.label chart.activeDatum.label) "is-active"}}
|
||||
|
||||
{{if (eq datum.value 0) "is-empty" "is-clickable"}}"
|
||||
>
|
||||
{{#if (and (gt datum.value 0) datum.legendLink)}}
|
||||
<LinkTo
|
||||
@route="jobs.job.allocations"
|
||||
@model={{@job}}
|
||||
@query={{datum.legendLink.queryParams}}
|
||||
>
|
||||
<JobPage::Parts::SummaryLegendItem
|
||||
@datum={{datum}}
|
||||
@index={{index}}
|
||||
/>
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<JobPage::Parts::SummaryLegendItem
|
||||
@datum={{datum}}
|
||||
@index={{index}}
|
||||
/>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</AllocationStatusBar>
|
||||
{{/if}}
|
|
@ -51,67 +51,6 @@
|
|||
</div>
|
||||
</a.head>
|
||||
<a.body>
|
||||
{{#if a.item.hasChildren}}
|
||||
<ChildrenStatusBar
|
||||
@allocationContainer={{a.item.summary}}
|
||||
@job={{a.item.summary}}
|
||||
@class="split-view" as |chart|
|
||||
>
|
||||
<ol data-test-legend class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li
|
||||
class="{{datum.className}}
|
||||
|
||||
{{if (eq datum.label chart.activeDatum.label) "is-active"}}
|
||||
|
||||
{{if (eq datum.value 0) "is-empty"}}"
|
||||
>
|
||||
<JobPage::Parts::SummaryLegendItem
|
||||
@datum={{datum}}
|
||||
@index={{index}}
|
||||
/>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</ChildrenStatusBar>
|
||||
{{else}}
|
||||
<AllocationStatusBar
|
||||
@allocationContainer={{a.item.summary}}
|
||||
@job={{this.job}}
|
||||
@onSliceClick={{this.onSliceClick}}
|
||||
@class="split-view" as |chart|
|
||||
>
|
||||
<ol data-test-legend class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li
|
||||
data-test-legend-label="{{datum.className}}"
|
||||
class="{{datum.className}}
|
||||
|
||||
{{if (eq datum.label chart.activeDatum.label) "is-active"}}
|
||||
|
||||
{{if (eq datum.value 0) "is-empty" "is-clickable"}}"
|
||||
>
|
||||
{{#if (and (gt datum.value 0) datum.legendLink)}}
|
||||
<LinkTo
|
||||
@route="jobs.job.allocations"
|
||||
@model={{this.job}}
|
||||
@query={{datum.legendLink.queryParams}}
|
||||
>
|
||||
<JobPage::Parts::SummaryLegendItem
|
||||
@datum={{datum}}
|
||||
@index={{index}}
|
||||
/>
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<JobPage::Parts::SummaryLegendItem
|
||||
@datum={{datum}}
|
||||
@index={{index}}
|
||||
/>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</AllocationStatusBar>
|
||||
{{/if}}
|
||||
<JobPage::Parts::SummaryChart @job={{a.item}} />
|
||||
</a.body>
|
||||
</ListAccordion>
|
|
@ -9,8 +9,9 @@
|
|||
<jobPage.ui.Title />
|
||||
<jobPage.ui.StatsBox />
|
||||
<jobPage.ui.DasRecommendations />
|
||||
<jobPage.ui.Summary />
|
||||
<jobPage.ui.StatusPanel @statusMode={{@statusMode}} @setStatusMode={{@setStatusMode}} />
|
||||
<jobPage.ui.PlacementFailures />
|
||||
{{!-- latestDeployment only included here for visual comparison during build-out --}}
|
||||
<jobPage.ui.LatestDeployment />
|
||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||
|
|
|
@ -38,6 +38,13 @@
|
|||
@selection={{this.selectionTaskGroup}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpTaskGroup"}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-allocation-version-facet
|
||||
@label="Job Version"
|
||||
@options={{this.optionsVersions}}
|
||||
@selection={{this.selectionVersion}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpVersion"}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,4 +12,6 @@
|
|||
currentPage=this.currentPage
|
||||
activeTask=this.activeTask
|
||||
setActiveTaskQueryParam=this.setActiveTaskQueryParam
|
||||
statusMode=this.statusMode
|
||||
setStatusMode=this.setStatusMode
|
||||
}}
|
|
@ -8,13 +8,20 @@ import faker from 'nomad-ui/mirage/faker';
|
|||
import { provide } from '../utils';
|
||||
|
||||
const UUIDS = provide(100, faker.random.uuid.bind(faker.random));
|
||||
const DEPLOYMENT_STATUSES = ['running', 'successful', 'paused', 'failed', 'cancelled'];
|
||||
const DEPLOYMENT_STATUSES = [
|
||||
'running',
|
||||
'successful',
|
||||
'paused',
|
||||
'failed',
|
||||
'cancelled',
|
||||
];
|
||||
|
||||
export default Factory.extend({
|
||||
id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]),
|
||||
id: (i) => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]),
|
||||
|
||||
jobId: null,
|
||||
versionNumber: null,
|
||||
groupDesiredTotal: null,
|
||||
|
||||
status: () => faker.helpers.randomize(DEPLOYMENT_STATUSES),
|
||||
statusDescription: () => faker.lorem.sentence(),
|
||||
|
@ -29,14 +36,20 @@ export default Factory.extend({
|
|||
|
||||
afterCreate(deployment, server) {
|
||||
const job = server.db.jobs.find(deployment.jobId);
|
||||
const groups = job.taskGroupIds.map(id =>
|
||||
server.create('deployment-task-group-summary', {
|
||||
const groups = job.taskGroupIds.map((id) => {
|
||||
let summary = server.create('deployment-task-group-summary', {
|
||||
deployment,
|
||||
name: server.db.taskGroups.find(id).name,
|
||||
desiredCanaries: 1,
|
||||
promoted: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
if (deployment.groupDesiredTotal) {
|
||||
summary.update({
|
||||
desiredTotal: deployment.groupDesiredTotal,
|
||||
});
|
||||
}
|
||||
return summary;
|
||||
});
|
||||
|
||||
deployment.update({
|
||||
deploymentTaskGroupSummaryIds: groups.mapBy('id'),
|
||||
|
|
|
@ -28,6 +28,40 @@ export default Factory.extend({
|
|||
return summary;
|
||||
}, {});
|
||||
},
|
||||
afterCreate(jobSummary, server) {
|
||||
// Update the summary alloc types to match server allocations with same job ID
|
||||
const jobAllocs = server.db.allocations.where({
|
||||
jobId: jobSummary.jobId,
|
||||
});
|
||||
let summary = jobSummary.groupNames.reduce((summary, group) => {
|
||||
summary[group] = {
|
||||
Queued: jobAllocs
|
||||
.filterBy('taskGroup', group)
|
||||
.filterBy('clientStatus', 'pending').length,
|
||||
Complete: jobAllocs
|
||||
.filterBy('taskGroup', group)
|
||||
.filterBy('clientStatus', 'complete').length,
|
||||
Failed: jobAllocs
|
||||
.filterBy('taskGroup', group)
|
||||
.filterBy('clientStatus', 'failed').length,
|
||||
Running: jobAllocs
|
||||
.filterBy('taskGroup', group)
|
||||
.filterBy('clientStatus', 'running').length,
|
||||
Starting: jobAllocs
|
||||
.filterBy('taskGroup', group)
|
||||
.filterBy('clientStatus', 'starting').length,
|
||||
Lost: jobAllocs
|
||||
.filterBy('taskGroup', group)
|
||||
.filterBy('clientStatus', 'lost').length,
|
||||
Unknown: jobAllocs
|
||||
.filterBy('taskGroup', group)
|
||||
.filterBy('clientStatus', 'unknown').length,
|
||||
};
|
||||
return summary;
|
||||
}, {});
|
||||
|
||||
jobSummary.update({ summary });
|
||||
},
|
||||
}),
|
||||
|
||||
withChildren: trait({
|
||||
|
|
|
@ -222,6 +222,7 @@ export default Factory.extend({
|
|||
withTaskServices: job.withTaskServices,
|
||||
createRecommendations: job.createRecommendations,
|
||||
shallow: job.shallow,
|
||||
allocStatusDistribution: job.allocStatusDistribution,
|
||||
};
|
||||
|
||||
if (job.groupTaskCount) {
|
||||
|
|
|
@ -17,5 +17,5 @@ export default Factory.extend({
|
|||
exitCode: () => null,
|
||||
time: () => faker.date.past(2 / 365, REF_TIME) * 1000000,
|
||||
|
||||
displayMessage: () => faker.lorem.sentence(),
|
||||
message: () => faker.lorem.sentence(),
|
||||
});
|
||||
|
|
|
@ -116,28 +116,84 @@ export default Factory.extend({
|
|||
});
|
||||
|
||||
if (group.createAllocations) {
|
||||
Array(group.count)
|
||||
.fill(null)
|
||||
.forEach((_, i) => {
|
||||
const props = {
|
||||
jobId: group.job.id,
|
||||
namespace: group.job.namespace,
|
||||
taskGroup: group.name,
|
||||
name: `${group.name}.[${i}]`,
|
||||
rescheduleSuccess: group.withRescheduling
|
||||
? faker.random.boolean()
|
||||
: null,
|
||||
rescheduleAttempts: group.withRescheduling
|
||||
? faker.random.number({ min: 1, max: 5 })
|
||||
: 0,
|
||||
};
|
||||
if (group.allocStatusDistribution) {
|
||||
const statusProbabilities = group.allocStatusDistribution || {
|
||||
running: 0.6,
|
||||
failed: 0.05,
|
||||
unknown: 0.25,
|
||||
lost: 0.1,
|
||||
};
|
||||
|
||||
if (group.withRescheduling) {
|
||||
server.create('allocation', 'rescheduled', props);
|
||||
} else {
|
||||
server.create('allocation', props);
|
||||
}
|
||||
const totalAllocations = group.count;
|
||||
const allocationsByStatus = {};
|
||||
|
||||
Object.entries(statusProbabilities).forEach(([status, prob]) => {
|
||||
allocationsByStatus[status] = Math.round(totalAllocations * prob);
|
||||
});
|
||||
|
||||
let currentStatusIndex = 0;
|
||||
const statusKeys = Object.keys(allocationsByStatus);
|
||||
|
||||
Array(totalAllocations)
|
||||
.fill(null)
|
||||
.forEach((_, i) => {
|
||||
let clientStatus;
|
||||
|
||||
while (allocationsByStatus[statusKeys[currentStatusIndex]] === 0) {
|
||||
currentStatusIndex++;
|
||||
}
|
||||
|
||||
clientStatus = statusKeys[currentStatusIndex];
|
||||
allocationsByStatus[clientStatus]--;
|
||||
|
||||
const props = {
|
||||
jobId: group.job.id,
|
||||
namespace: group.job.namespace,
|
||||
taskGroup: group.name,
|
||||
name: `${group.name}.[${i}]`,
|
||||
rescheduleSuccess: group.withRescheduling
|
||||
? faker.random.boolean()
|
||||
: null,
|
||||
rescheduleAttempts: group.withRescheduling
|
||||
? faker.random.number({ min: 1, max: 5 })
|
||||
: 0,
|
||||
clientStatus,
|
||||
deploymentStatus: {
|
||||
Canary: false,
|
||||
Healthy: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (group.withRescheduling) {
|
||||
server.create('allocation', 'rescheduled', props);
|
||||
} else {
|
||||
server.create('allocation', props);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Array(group.count)
|
||||
.fill(null)
|
||||
.forEach((_, i) => {
|
||||
const props = {
|
||||
jobId: group.job.id,
|
||||
namespace: group.job.namespace,
|
||||
taskGroup: group.name,
|
||||
name: `${group.name}.[${i}]`,
|
||||
rescheduleSuccess: group.withRescheduling
|
||||
? faker.random.boolean()
|
||||
: null,
|
||||
rescheduleAttempts: group.withRescheduling
|
||||
? faker.random.number({ min: 1, max: 5 })
|
||||
: 0,
|
||||
};
|
||||
|
||||
if (group.withRescheduling) {
|
||||
server.create('allocation', 'rescheduled', props);
|
||||
} else {
|
||||
server.create('allocation', props);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (group.withServices) {
|
||||
|
|
|
@ -73,6 +73,94 @@ function smallCluster(server) {
|
|||
id: 'service-haver',
|
||||
namespaceId: 'default',
|
||||
});
|
||||
server.create('job', {
|
||||
createAllocations: true,
|
||||
groupTaskCount: 150,
|
||||
shallow: true,
|
||||
allocStatusDistribution: {
|
||||
running: 0.5,
|
||||
failed: 0.05,
|
||||
unknown: 0.2,
|
||||
lost: 0.1,
|
||||
complete: 0.1,
|
||||
pending: 0.05,
|
||||
},
|
||||
name: 'mixed-alloc-job',
|
||||
id: 'mixed-alloc-job',
|
||||
namespaceId: 'default',
|
||||
type: 'service',
|
||||
activeDeployment: true,
|
||||
});
|
||||
|
||||
//#region Active Deployment
|
||||
|
||||
const activelyDeployingJobGroups = 2;
|
||||
const activelyDeployingTasksPerGroup = 100;
|
||||
|
||||
const activelyDeployingJob = server.create('job', {
|
||||
createAllocations: true,
|
||||
groupTaskCount: activelyDeployingTasksPerGroup,
|
||||
shallow: true,
|
||||
resourceSpec: Array(activelyDeployingJobGroups).fill(['M: 257, C: 500']),
|
||||
noDeployments: true, // manually created below
|
||||
activeDeployment: true,
|
||||
allocStatusDistribution: {
|
||||
running: 0.6,
|
||||
failed: 0.05,
|
||||
unknown: 0.05,
|
||||
lost: 0,
|
||||
complete: 0,
|
||||
pending: 0.3,
|
||||
},
|
||||
name: 'actively-deploying-job',
|
||||
id: 'actively-deploying-job',
|
||||
namespaceId: 'default',
|
||||
type: 'service',
|
||||
});
|
||||
|
||||
server.create('deployment', false, 'active', {
|
||||
jobId: activelyDeployingJob.id,
|
||||
groupDesiredTotal: activelyDeployingTasksPerGroup,
|
||||
versionNumber: 1,
|
||||
status: 'running',
|
||||
});
|
||||
server.createList('allocation', 25, {
|
||||
jobId: activelyDeployingJob.id,
|
||||
jobVersion: 0,
|
||||
clientStatus: 'running',
|
||||
});
|
||||
|
||||
// Manipulate the above job to show a nice distribution of running, canary, etc. allocs
|
||||
let activelyDeployingJobAllocs = server.schema.allocations
|
||||
.all()
|
||||
.filter((a) => a.jobId === activelyDeployingJob.id);
|
||||
activelyDeployingJobAllocs.models
|
||||
.filter((a) => a.clientStatus === 'running')
|
||||
.slice(0, 10)
|
||||
.forEach((a) =>
|
||||
a.update({ deploymentStatus: { Healthy: false, Canary: true } })
|
||||
);
|
||||
activelyDeployingJobAllocs.models
|
||||
.filter((a) => a.clientStatus === 'running')
|
||||
.slice(10, 20)
|
||||
.forEach((a) =>
|
||||
a.update({ deploymentStatus: { Healthy: true, Canary: true } })
|
||||
);
|
||||
activelyDeployingJobAllocs.models
|
||||
.filter((a) => a.clientStatus === 'running')
|
||||
.slice(20, 65)
|
||||
.forEach((a) =>
|
||||
a.update({ deploymentStatus: { Healthy: true, Canary: false } })
|
||||
);
|
||||
activelyDeployingJobAllocs.models
|
||||
.filter((a) => a.clientStatus === 'pending')
|
||||
.slice(0, 10)
|
||||
.forEach((a) =>
|
||||
a.update({ deploymentStatus: { Healthy: true, Canary: true } })
|
||||
);
|
||||
|
||||
//#endregion Active Deployment
|
||||
|
||||
server.createList('allocFile', 5);
|
||||
server.create('allocFile', 'dir', { depth: 2 });
|
||||
server.createList('csi-plugin', 2);
|
||||
|
|
|
@ -216,7 +216,7 @@ module('Acceptance | allocation detail', function (hooks) {
|
|||
|
||||
assert.equal(taskRow.name, task.name, 'Name');
|
||||
assert.equal(taskRow.state, task.state, 'State');
|
||||
assert.equal(taskRow.message, event.displayMessage, 'Event Message');
|
||||
assert.equal(taskRow.message, event.message, 'Event Message');
|
||||
assert.equal(
|
||||
taskRow.time,
|
||||
moment(event.time / 1000000).format("MMM DD, 'YY HH:mm:ss ZZ"),
|
||||
|
|
|
@ -0,0 +1,487 @@
|
|||
// @ts-check
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
|
||||
import {
|
||||
click,
|
||||
visit,
|
||||
find,
|
||||
findAll,
|
||||
fillIn,
|
||||
triggerEvent,
|
||||
} from '@ember/test-helpers';
|
||||
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import faker from 'nomad-ui/mirage/faker';
|
||||
import percySnapshot from '@percy/ember';
|
||||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
// TODO: Mirage is not type-friendly / assigns "server" as a global. Try to work around this shortcoming.
|
||||
|
||||
module('Acceptance | job status panel', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
server.create('node');
|
||||
});
|
||||
|
||||
test('Status panel lets you switch between Current and Historical', async function (assert) {
|
||||
assert.expect(5);
|
||||
let job = server.create('job', {
|
||||
status: 'running',
|
||||
datacenters: ['*'],
|
||||
type: 'service',
|
||||
createAllocations: true,
|
||||
});
|
||||
|
||||
await visit(`/jobs/${job.id}`);
|
||||
assert.dom('.job-status-panel').exists();
|
||||
await a11yAudit(assert);
|
||||
await percySnapshot(assert);
|
||||
|
||||
assert
|
||||
.dom('[data-test-status-mode="current"]')
|
||||
.exists('Current mode by default');
|
||||
|
||||
await click('[data-test-status-mode-current]');
|
||||
|
||||
assert
|
||||
.dom('[data-test-status-mode="current"]')
|
||||
.exists('Clicking active mode makes no change');
|
||||
|
||||
await click('[data-test-status-mode-historical]');
|
||||
|
||||
assert
|
||||
.dom('[data-test-status-mode="historical"]')
|
||||
.exists('Lets you switch to historical mode');
|
||||
});
|
||||
|
||||
test('Status panel observes query parameters for current/historical', async function (assert) {
|
||||
assert.expect(2);
|
||||
let job = server.create('job', {
|
||||
status: 'running',
|
||||
datacenters: ['*'],
|
||||
type: 'service',
|
||||
createAllocations: true,
|
||||
});
|
||||
|
||||
await visit(`/jobs/${job.id}?statusMode=historical`);
|
||||
assert.dom('.job-status-panel').exists();
|
||||
|
||||
assert
|
||||
.dom('[data-test-status-mode="historical"]')
|
||||
.exists('Historical mode when rendered with queryParams');
|
||||
});
|
||||
|
||||
test('Status Panel shows accurate number and types of ungrouped allocation blocks', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
faker.seed(1);
|
||||
|
||||
let groupTaskCount = 10;
|
||||
|
||||
let job = server.create('job', {
|
||||
status: 'running',
|
||||
datacenters: ['*'],
|
||||
type: 'service',
|
||||
resourceSpec: ['M: 256, C: 500'], // a single group
|
||||
createAllocations: true,
|
||||
allocStatusDistribution: {
|
||||
running: 1,
|
||||
failed: 0,
|
||||
unknown: 0,
|
||||
lost: 0,
|
||||
},
|
||||
groupTaskCount,
|
||||
shallow: true,
|
||||
});
|
||||
|
||||
await visit(`/jobs/${job.id}`);
|
||||
assert.dom('.job-status-panel').exists();
|
||||
|
||||
let jobAllocCount = server.db.allocations.where({
|
||||
jobId: job.id,
|
||||
}).length;
|
||||
|
||||
assert.equal(
|
||||
jobAllocCount,
|
||||
groupTaskCount * job.taskGroups.length,
|
||||
'Correect number of allocs generated (metatest)'
|
||||
);
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.running')
|
||||
.exists(
|
||||
{ count: jobAllocCount },
|
||||
`All ${jobAllocCount} allocations are represented in the status panel`
|
||||
);
|
||||
|
||||
groupTaskCount = 20;
|
||||
|
||||
job = server.create('job', {
|
||||
status: 'running',
|
||||
datacenters: ['*'],
|
||||
type: 'service',
|
||||
resourceSpec: ['M: 256, C: 500'], // a single group
|
||||
createAllocations: true,
|
||||
allocStatusDistribution: {
|
||||
running: 0.5,
|
||||
failed: 0.5,
|
||||
unknown: 0,
|
||||
lost: 0,
|
||||
},
|
||||
groupTaskCount,
|
||||
noActiveDeployment: true,
|
||||
shallow: true,
|
||||
});
|
||||
|
||||
await visit(`/jobs/${job.id}`);
|
||||
assert.dom('.job-status-panel').exists();
|
||||
|
||||
let runningAllocCount = server.db.allocations.where({
|
||||
jobId: job.id,
|
||||
clientStatus: 'running',
|
||||
}).length;
|
||||
|
||||
let failedAllocCount = server.db.allocations.where({
|
||||
jobId: job.id,
|
||||
clientStatus: 'failed',
|
||||
}).length;
|
||||
|
||||
assert.equal(
|
||||
runningAllocCount + failedAllocCount,
|
||||
groupTaskCount * job.taskGroups.length,
|
||||
'Correect number of allocs generated (metatest)'
|
||||
);
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.running')
|
||||
.exists(
|
||||
{ count: runningAllocCount },
|
||||
`All ${runningAllocCount} running allocations are represented in the status panel`
|
||||
);
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.failed')
|
||||
.exists(
|
||||
{ count: failedAllocCount },
|
||||
`All ${failedAllocCount} failed allocations are represented in the status panel`
|
||||
);
|
||||
await percySnapshot(assert);
|
||||
});
|
||||
|
||||
test('Status Panel groups allocations when they get past a threshold', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
faker.seed(1);
|
||||
|
||||
let groupTaskCount = 20;
|
||||
|
||||
let job = server.create('job', {
|
||||
status: 'running',
|
||||
datacenters: ['*'],
|
||||
type: 'service',
|
||||
resourceSpec: ['M: 256, C: 500'], // a single group
|
||||
createAllocations: true,
|
||||
allocStatusDistribution: {
|
||||
running: 1,
|
||||
failed: 0,
|
||||
unknown: 0,
|
||||
lost: 0,
|
||||
},
|
||||
groupTaskCount,
|
||||
shallow: true,
|
||||
});
|
||||
|
||||
await visit(`/jobs/${job.id}`);
|
||||
assert.dom('.job-status-panel').exists();
|
||||
|
||||
let jobAllocCount = server.db.allocations.where({
|
||||
jobId: job.id,
|
||||
}).length;
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.running')
|
||||
.exists(
|
||||
{ count: jobAllocCount },
|
||||
`All ${jobAllocCount} allocations are represented in the status panel, ungrouped`
|
||||
);
|
||||
|
||||
groupTaskCount = 40;
|
||||
|
||||
job = server.create('job', {
|
||||
status: 'running',
|
||||
datacenters: ['*'],
|
||||
type: 'service',
|
||||
resourceSpec: ['M: 256, C: 500'], // a single group
|
||||
createAllocations: true,
|
||||
allocStatusDistribution: {
|
||||
running: 1,
|
||||
failed: 0,
|
||||
unknown: 0,
|
||||
lost: 0,
|
||||
},
|
||||
groupTaskCount,
|
||||
shallow: true,
|
||||
});
|
||||
|
||||
await visit(`/jobs/${job.id}`);
|
||||
assert.dom('.job-status-panel').exists();
|
||||
|
||||
jobAllocCount = server.db.allocations.where({
|
||||
jobId: job.id,
|
||||
}).length;
|
||||
|
||||
// At standard test resolution, 40 allocations will attempt to display 20 ungrouped, and 20 grouped.
|
||||
let desiredUngroupedAllocCount = 20;
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.running')
|
||||
.exists(
|
||||
{ count: desiredUngroupedAllocCount },
|
||||
`${desiredUngroupedAllocCount} allocations are represented ungrouped`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.represented-allocation.rest')
|
||||
.exists('Allocations are numerous enough that a summary block exists');
|
||||
assert
|
||||
.dom('.represented-allocation.rest')
|
||||
.hasText(
|
||||
`+${groupTaskCount - desiredUngroupedAllocCount}`,
|
||||
'Summary block has the correct number of grouped allocs'
|
||||
);
|
||||
|
||||
await percySnapshot(assert);
|
||||
});
|
||||
|
||||
test('Status Panel groups allocations when they get past a threshold, multiple statuses', async function (assert) {
|
||||
let groupTaskCount = 50;
|
||||
|
||||
let job = server.create('job', {
|
||||
status: 'running',
|
||||
datacenters: ['*'],
|
||||
type: 'service',
|
||||
resourceSpec: ['M: 256, C: 500'], // a single group
|
||||
createAllocations: true,
|
||||
allocStatusDistribution: {
|
||||
running: 0.5,
|
||||
failed: 0.3,
|
||||
pending: 0.1,
|
||||
lost: 0.1,
|
||||
},
|
||||
groupTaskCount,
|
||||
shallow: true,
|
||||
});
|
||||
|
||||
await visit(`/jobs/${job.id}`);
|
||||
assert.dom('.job-status-panel').exists();
|
||||
|
||||
// With 50 allocs split across 4 statuses distributed as above, we can expect 25 running, 16 failed, 6 pending, and 4 remaining.
|
||||
// At standard test resolution, each status will be ungrouped/grouped as follows:
|
||||
// 25 running: 9 ungrouped, 17 grouped
|
||||
// 15 failed: 5 ungrouped, 10 grouped
|
||||
// 5 pending: 0 ungrouped, 5 grouped
|
||||
// 5 lost: 0 ungrouped, 5 grouped. Represented as "Unplaced"
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.running')
|
||||
.exists({ count: 9 }, '9 running allocations are represented ungrouped');
|
||||
assert
|
||||
.dom('.represented-allocation.rest.running')
|
||||
.exists(
|
||||
'Running allocations are numerous enough that a summary block exists'
|
||||
);
|
||||
assert
|
||||
.dom('.represented-allocation.rest.running')
|
||||
.hasText(
|
||||
'+16',
|
||||
'Summary block has the correct number of grouped running allocs'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.failed')
|
||||
.exists({ count: 5 }, '5 failed allocations are represented ungrouped');
|
||||
assert
|
||||
.dom('.represented-allocation.rest.failed')
|
||||
.exists(
|
||||
'Failed allocations are numerous enough that a summary block exists'
|
||||
);
|
||||
assert
|
||||
.dom('.represented-allocation.rest.failed')
|
||||
.hasText(
|
||||
'+10',
|
||||
'Summary block has the correct number of grouped failed allocs'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.pending')
|
||||
.exists({ count: 0 }, '0 pending allocations are represented ungrouped');
|
||||
assert
|
||||
.dom('.represented-allocation.rest.pending')
|
||||
.exists(
|
||||
'pending allocations are numerous enough that a summary block exists'
|
||||
);
|
||||
assert
|
||||
.dom('.represented-allocation.rest.pending')
|
||||
.hasText(
|
||||
'5',
|
||||
'Summary block has the correct number of grouped pending allocs'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.unplaced')
|
||||
.exists({ count: 0 }, '0 unplaced allocations are represented ungrouped');
|
||||
assert
|
||||
.dom('.represented-allocation.rest.unplaced')
|
||||
.exists(
|
||||
'Unplaced allocations are numerous enough that a summary block exists'
|
||||
);
|
||||
assert
|
||||
.dom('.represented-allocation.rest.unplaced')
|
||||
.hasText(
|
||||
'5',
|
||||
'Summary block has the correct number of grouped unplaced allocs'
|
||||
);
|
||||
await percySnapshot(
|
||||
'Status Panel groups allocations when they get past a threshold, multiple statuses (full width)'
|
||||
);
|
||||
|
||||
// Simulate a window resize event; will recompute how many of each ought to be grouped.
|
||||
|
||||
// At 1100px, only running and failed allocations have some ungrouped allocs
|
||||
find('.page-body').style.width = '1100px';
|
||||
await triggerEvent(window, 'resize');
|
||||
|
||||
await percySnapshot(
|
||||
'Status Panel groups allocations when they get past a threshold, multiple statuses (1100px)'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.running')
|
||||
.exists({ count: 7 }, '7 running allocations are represented ungrouped');
|
||||
assert
|
||||
.dom('.represented-allocation.rest.running')
|
||||
.exists(
|
||||
'Running allocations are numerous enough that a summary block exists'
|
||||
);
|
||||
assert
|
||||
.dom('.represented-allocation.rest.running')
|
||||
.hasText(
|
||||
'+18',
|
||||
'Summary block has the correct number of grouped running allocs'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.failed')
|
||||
.exists({ count: 4 }, '4 failed allocations are represented ungrouped');
|
||||
assert
|
||||
.dom('.represented-allocation.rest.failed')
|
||||
.exists(
|
||||
'Failed allocations are numerous enough that a summary block exists'
|
||||
);
|
||||
assert
|
||||
.dom('.represented-allocation.rest.failed')
|
||||
.hasText(
|
||||
'+11',
|
||||
'Summary block has the correct number of grouped failed allocs'
|
||||
);
|
||||
|
||||
// At 500px, only running allocations have some ungrouped allocs. The rest are all fully grouped.
|
||||
find('.page-body').style.width = '800px';
|
||||
await triggerEvent(window, 'resize');
|
||||
|
||||
await percySnapshot(
|
||||
'Status Panel groups allocations when they get past a threshold, multiple statuses (500px)'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.running')
|
||||
.exists({ count: 4 }, '4 running allocations are represented ungrouped');
|
||||
assert
|
||||
.dom('.represented-allocation.rest.running')
|
||||
.exists(
|
||||
'Running allocations are numerous enough that a summary block exists'
|
||||
);
|
||||
assert
|
||||
.dom('.represented-allocation.rest.running')
|
||||
.hasText(
|
||||
'+21',
|
||||
'Summary block has the correct number of grouped running allocs'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom('.ungrouped-allocs .represented-allocation.failed')
|
||||
.doesNotExist('no failed allocations are represented ungrouped');
|
||||
assert
|
||||
.dom('.represented-allocation.rest.failed')
|
||||
.exists(
|
||||
'Failed allocations are numerous enough that a summary block exists'
|
||||
);
|
||||
assert
|
||||
.dom('.represented-allocation.rest.failed')
|
||||
.hasText(
|
||||
'15',
|
||||
'Summary block has the correct number of grouped failed allocs'
|
||||
);
|
||||
});
|
||||
|
||||
module('deployment history', function () {
|
||||
test('Deployment history can be searched', async function (assert) {
|
||||
faker.seed(1);
|
||||
|
||||
let groupTaskCount = 10;
|
||||
|
||||
let job = server.create('job', {
|
||||
status: 'running',
|
||||
datacenters: ['*'],
|
||||
type: 'service',
|
||||
resourceSpec: ['M: 256, C: 500'], // a single group
|
||||
createAllocations: true,
|
||||
allocStatusDistribution: {
|
||||
running: 1,
|
||||
failed: 0,
|
||||
unknown: 0,
|
||||
lost: 0,
|
||||
},
|
||||
groupTaskCount,
|
||||
shallow: true,
|
||||
activeDeployment: true,
|
||||
version: 0,
|
||||
});
|
||||
|
||||
let state = server.create('task-state');
|
||||
state.events = server.schema.taskEvents.where({ taskStateId: state.id });
|
||||
|
||||
server.schema.allocations.where({ jobId: job.id }).update({
|
||||
taskStateIds: [state.id],
|
||||
jobVersion: 0,
|
||||
});
|
||||
|
||||
await visit(`/jobs/${job.id}`);
|
||||
assert.dom('.job-status-panel').exists();
|
||||
|
||||
const serverEvents = server.schema.taskEvents.where({
|
||||
taskStateId: state.id,
|
||||
});
|
||||
const shownEvents = findAll('.timeline-object');
|
||||
const jobAllocations = server.db.allocations.where({ jobId: job.id });
|
||||
assert.equal(
|
||||
shownEvents.length,
|
||||
serverEvents.length * jobAllocations.length,
|
||||
'All events are shown'
|
||||
);
|
||||
|
||||
await fillIn(
|
||||
'[data-test-history-search] input',
|
||||
serverEvents.models[0].message
|
||||
);
|
||||
assert.equal(
|
||||
findAll('.timeline-object').length,
|
||||
jobAllocations.length,
|
||||
'Only events matching the search are shown'
|
||||
);
|
||||
|
||||
await fillIn('[data-test-history-search] input', 'foo bar baz');
|
||||
assert
|
||||
.dom('[data-test-history-search-no-match]')
|
||||
.exists('No match message is shown');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -46,7 +46,7 @@ module('Acceptance | jobs list', function (hooks) {
|
|||
test('/jobs should list the first page of jobs sorted by modify index', async function (assert) {
|
||||
faker.seed(1);
|
||||
const jobsCount = JobsList.pageSize + 1;
|
||||
server.createList('job', jobsCount, { createAllocations: false });
|
||||
server.createList('job', jobsCount, { createAllocations: true });
|
||||
|
||||
await JobsList.visit();
|
||||
|
||||
|
|
|
@ -222,7 +222,7 @@ module('Acceptance | task detail', function (hooks) {
|
|||
'Event timestamp'
|
||||
);
|
||||
assert.equal(recentEvent.type, event.type, 'Event type');
|
||||
assert.equal(recentEvent.message, event.displayMessage, 'Event message');
|
||||
assert.equal(recentEvent.message, event.message, 'Event message');
|
||||
});
|
||||
|
||||
test('when the allocation is not found, the application errors', async function (assert) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
currentRouteName,
|
||||
currentURL,
|
||||
visit,
|
||||
find,
|
||||
} from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
|
@ -17,6 +18,12 @@ 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'];
|
||||
|
||||
async function switchToHistorical() {
|
||||
await JobDetail.statusModes.historical.click();
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -120,6 +127,9 @@ export default function moduleForJob(
|
|||
|
||||
if (context === 'allocations') {
|
||||
test('allocations for the job are shown in the overview', async function (assert) {
|
||||
if (jobTypesWithStatusPanel.includes(job.type)) {
|
||||
await switchToHistorical(job);
|
||||
}
|
||||
assert.ok(
|
||||
JobDetail.allocationsSummary.isPresent,
|
||||
'Allocations are shown in the summary section'
|
||||
|
@ -157,9 +167,11 @@ export default function moduleForJob(
|
|||
});
|
||||
|
||||
test('clicking legend item navigates to a pre-filtered allocations table', async function (assert) {
|
||||
const legendItem =
|
||||
JobDetail.allocationsSummary.legend.clickableItems[1];
|
||||
const status = legendItem.label;
|
||||
if (jobTypesWithStatusPanel.includes(job.type)) {
|
||||
await switchToHistorical(job);
|
||||
}
|
||||
const legendItem = find('.legend li.is-clickable');
|
||||
const status = legendItem.getAttribute('data-test-legend-label');
|
||||
await legendItem.click();
|
||||
|
||||
const encodedStatus = encodeURIComponent(JSON.stringify([status]));
|
||||
|
@ -176,7 +188,10 @@ export default function moduleForJob(
|
|||
});
|
||||
|
||||
test('clicking in a slice takes you to a pre-filtered allocations table', async function (assert) {
|
||||
const slice = JobDetail.allocationsSummary.slices[1];
|
||||
if (jobTypesWithStatusPanel.includes(job.type)) {
|
||||
await switchToHistorical(job);
|
||||
}
|
||||
const slice = JobDetail.allocationsSummary.slices[0];
|
||||
const status = slice.label;
|
||||
await slice.click();
|
||||
|
||||
|
|
|
@ -132,7 +132,6 @@ module('Integration | Component | job-editor', function (hooks) {
|
|||
|
||||
await renderNewJob(this, job);
|
||||
await planJob(spec);
|
||||
console.log('wait');
|
||||
const requests = this.server.pretender.handledRequests.mapBy('url');
|
||||
assert.notOk(
|
||||
requests.includes('/v1/jobs/parse'),
|
||||
|
|
|
@ -44,7 +44,10 @@ module('Integration | Component | job-page/service', function (hooks) {
|
|||
@sortProperty={{sortProperty}}
|
||||
@sortDescending={{sortDescending}}
|
||||
@currentPage={{currentPage}}
|
||||
@gotoJob={{gotoJob}} />
|
||||
@gotoJob={{gotoJob}}
|
||||
@statusMode={{statusMode}}
|
||||
@setStatusMode={{setStatusMode}}
|
||||
/>
|
||||
`;
|
||||
|
||||
const commonProperties = (job) => ({
|
||||
|
@ -53,6 +56,8 @@ module('Integration | Component | job-page/service', function (hooks) {
|
|||
sortDescending: true,
|
||||
currentPage: 1,
|
||||
gotoJob() {},
|
||||
statusMode: 'current',
|
||||
setStatusMode() {},
|
||||
});
|
||||
|
||||
const makeMirageJob = (server, props = {}) =>
|
||||
|
@ -272,7 +277,11 @@ module('Integration | Component | job-page/service', function (hooks) {
|
|||
'The error message mentions ACLs'
|
||||
);
|
||||
|
||||
await componentA11yAudit(this.element, assert);
|
||||
await componentA11yAudit(
|
||||
this.element,
|
||||
assert,
|
||||
'scrollable-region-focusable'
|
||||
); //keyframe animation fades from opacity 0
|
||||
|
||||
await click('[data-test-job-error-close]');
|
||||
|
||||
|
@ -335,7 +344,11 @@ module('Integration | Component | job-page/service', function (hooks) {
|
|||
'The error message mentions ACLs'
|
||||
);
|
||||
|
||||
await componentA11yAudit(this.element, assert);
|
||||
await componentA11yAudit(
|
||||
this.element,
|
||||
assert,
|
||||
'scrollable-region-focusable'
|
||||
); //keyframe animation fades from opacity 0
|
||||
|
||||
await click('[data-test-job-error-close]');
|
||||
|
||||
|
|
|
@ -0,0 +1,408 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { find, render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
|
||||
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
import percySnapshot from '@percy/ember';
|
||||
|
||||
module(
|
||||
'Integration | Component | job status panel | active deployment',
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
fragmentSerializerInitializer(this.owner);
|
||||
window.localStorage.clear();
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
test('there is no latest deployment section when the job has no deployments', async function (assert) {
|
||||
this.server.create('job', {
|
||||
type: 'service',
|
||||
noDeployments: true,
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
await this.store.findAll('job');
|
||||
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
await render(hbs`
|
||||
<JobStatus::Panel @job={{this.job}} />)
|
||||
`);
|
||||
|
||||
assert.notOk(find('.active-deployment'), 'No active deployment');
|
||||
});
|
||||
|
||||
test('the latest deployment section shows up for the currently running deployment: Ungrouped Allocations (small cluster)', async function (assert) {
|
||||
assert.expect(25);
|
||||
|
||||
this.server.create('node');
|
||||
|
||||
const NUMBER_OF_GROUPS = 2;
|
||||
const ALLOCS_PER_GROUP = 10;
|
||||
const allocStatusDistribution = {
|
||||
running: 0.5,
|
||||
failed: 0.2,
|
||||
unknown: 0.1,
|
||||
lost: 0,
|
||||
complete: 0.1,
|
||||
pending: 0.1,
|
||||
};
|
||||
|
||||
const job = await this.server.create('job', {
|
||||
type: 'service',
|
||||
createAllocations: true,
|
||||
noDeployments: true, // manually created below
|
||||
activeDeployment: true,
|
||||
groupTaskCount: ALLOCS_PER_GROUP,
|
||||
shallow: true,
|
||||
resourceSpec: Array(NUMBER_OF_GROUPS).fill(['M: 257, C: 500']), // length of this array determines number of groups
|
||||
allocStatusDistribution,
|
||||
});
|
||||
|
||||
const jobRecord = await this.store.find(
|
||||
'job',
|
||||
JSON.stringify([job.id, 'default'])
|
||||
);
|
||||
await this.server.create('deployment', false, 'active', {
|
||||
jobId: job.id,
|
||||
groupDesiredTotal: ALLOCS_PER_GROUP,
|
||||
versionNumber: 1,
|
||||
status: 'failed',
|
||||
});
|
||||
|
||||
const OLD_ALLOCATIONS_TO_SHOW = 25;
|
||||
const OLD_ALLOCATIONS_TO_COMPLETE = 5;
|
||||
|
||||
this.server.createList('allocation', OLD_ALLOCATIONS_TO_SHOW, {
|
||||
jobId: job.id,
|
||||
jobVersion: 0,
|
||||
clientStatus: 'running',
|
||||
});
|
||||
|
||||
this.set('job', jobRecord);
|
||||
await this.get('job.allocations');
|
||||
|
||||
await render(hbs`
|
||||
<JobStatus::Panel @job={{this.job}} />
|
||||
`);
|
||||
|
||||
// Initially no active deployment
|
||||
assert.notOk(
|
||||
find('.active-deployment'),
|
||||
'Does not show an active deployment when latest is failed'
|
||||
);
|
||||
|
||||
const deployment = await this.get('job.latestDeployment');
|
||||
|
||||
await this.set('job.latestDeployment.status', 'running');
|
||||
|
||||
assert.ok(
|
||||
find('.active-deployment'),
|
||||
'Shows an active deployment if latest status is Running'
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
find('.active-deployment').classList.contains('is-info'),
|
||||
'Running deployment gets the is-info class'
|
||||
);
|
||||
|
||||
// Half the shown allocations are running, 1 is pending, 1 is failed; none are canaries or healthy.
|
||||
// The rest (lost, unknown, etc.) all show up as "Unplaced"
|
||||
assert
|
||||
.dom('.new-allocations .allocation-status-row .represented-allocation')
|
||||
.exists(
|
||||
{ count: NUMBER_OF_GROUPS * ALLOCS_PER_GROUP },
|
||||
'All allocations are shown (ungrouped)'
|
||||
);
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.running'
|
||||
)
|
||||
.exists(
|
||||
{
|
||||
count:
|
||||
NUMBER_OF_GROUPS *
|
||||
ALLOCS_PER_GROUP *
|
||||
allocStatusDistribution.running,
|
||||
},
|
||||
'Correct number of running allocations are shown'
|
||||
);
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.running.canary'
|
||||
)
|
||||
.exists({ count: 0 }, 'No running canaries shown by default');
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.running.healthy'
|
||||
)
|
||||
.exists({ count: 0 }, 'No running healthy shown by default');
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.failed'
|
||||
)
|
||||
.exists(
|
||||
{
|
||||
count:
|
||||
NUMBER_OF_GROUPS *
|
||||
ALLOCS_PER_GROUP *
|
||||
allocStatusDistribution.failed,
|
||||
},
|
||||
'Correct number of failed allocations are shown'
|
||||
);
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.failed.canary'
|
||||
)
|
||||
.exists({ count: 0 }, 'No failed canaries shown by default');
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.pending'
|
||||
)
|
||||
.exists(
|
||||
{
|
||||
count:
|
||||
NUMBER_OF_GROUPS *
|
||||
ALLOCS_PER_GROUP *
|
||||
allocStatusDistribution.pending,
|
||||
},
|
||||
'Correct number of pending allocations are shown'
|
||||
);
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.pending.canary'
|
||||
)
|
||||
.exists({ count: 0 }, 'No pending canaries shown by default');
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.unplaced'
|
||||
)
|
||||
.exists(
|
||||
{
|
||||
count:
|
||||
NUMBER_OF_GROUPS *
|
||||
ALLOCS_PER_GROUP *
|
||||
(allocStatusDistribution.lost +
|
||||
allocStatusDistribution.unknown +
|
||||
allocStatusDistribution.complete),
|
||||
},
|
||||
'Correct number of unplaced allocations are shown'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-new-allocation-tally]').textContent.trim(),
|
||||
`New allocations: ${
|
||||
this.job.allocations.filter(
|
||||
(a) =>
|
||||
a.clientStatus === 'running' &&
|
||||
a.deploymentStatus?.Healthy === true
|
||||
).length
|
||||
}/${deployment.get('desiredTotal')} running and healthy`,
|
||||
'Summary text shows accurate numbers when 0 are running/healthy'
|
||||
);
|
||||
|
||||
let NUMBER_OF_RUNNING_CANARIES = 2;
|
||||
let NUMBER_OF_RUNNING_HEALTHY = 5;
|
||||
let NUMBER_OF_FAILED_CANARIES = 1;
|
||||
let NUMBER_OF_PENDING_CANARIES = 1;
|
||||
|
||||
// Set some allocs to canary, and to healthy
|
||||
this.get('job.allocations')
|
||||
.filter((a) => a.clientStatus === 'running')
|
||||
.slice(0, NUMBER_OF_RUNNING_CANARIES)
|
||||
.forEach((alloc) =>
|
||||
alloc.set('deploymentStatus', {
|
||||
Canary: true,
|
||||
Healthy: alloc.deploymentStatus?.Healthy,
|
||||
})
|
||||
);
|
||||
this.get('job.allocations')
|
||||
.filter((a) => a.clientStatus === 'running')
|
||||
.slice(0, NUMBER_OF_RUNNING_HEALTHY)
|
||||
.forEach((alloc) =>
|
||||
alloc.set('deploymentStatus', {
|
||||
Canary: alloc.deploymentStatus?.Canary,
|
||||
Healthy: true,
|
||||
})
|
||||
);
|
||||
this.get('job.allocations')
|
||||
.filter((a) => a.clientStatus === 'failed')
|
||||
.slice(0, NUMBER_OF_FAILED_CANARIES)
|
||||
.forEach((alloc) =>
|
||||
alloc.set('deploymentStatus', {
|
||||
Canary: true,
|
||||
Healthy: alloc.deploymentStatus?.Healthy,
|
||||
})
|
||||
);
|
||||
this.get('job.allocations')
|
||||
.filter((a) => a.clientStatus === 'pending')
|
||||
.slice(0, NUMBER_OF_PENDING_CANARIES)
|
||||
.forEach((alloc) =>
|
||||
alloc.set('deploymentStatus', {
|
||||
Canary: true,
|
||||
Healthy: alloc.deploymentStatus?.Healthy,
|
||||
})
|
||||
);
|
||||
|
||||
await render(hbs`
|
||||
<JobStatus::Panel @job={{this.job}} />
|
||||
`);
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.running.canary'
|
||||
)
|
||||
.exists(
|
||||
{ count: NUMBER_OF_RUNNING_CANARIES },
|
||||
'Running Canaries shown when deployment info dictates'
|
||||
);
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.running.healthy'
|
||||
)
|
||||
.exists(
|
||||
{ count: NUMBER_OF_RUNNING_HEALTHY },
|
||||
'Running Healthy allocs shown when deployment info dictates'
|
||||
);
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.failed.canary'
|
||||
)
|
||||
.exists(
|
||||
{ count: NUMBER_OF_FAILED_CANARIES },
|
||||
'Failed Canaries shown when deployment info dictates'
|
||||
);
|
||||
assert
|
||||
.dom(
|
||||
'.new-allocations .allocation-status-row .represented-allocation.pending.canary'
|
||||
)
|
||||
.exists(
|
||||
{ count: NUMBER_OF_PENDING_CANARIES },
|
||||
'Pending Canaries shown when deployment info dictates'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-new-allocation-tally]').textContent.trim(),
|
||||
`New allocations: ${
|
||||
this.job.allocations.filter(
|
||||
(a) =>
|
||||
a.clientStatus === 'running' &&
|
||||
a.deploymentStatus?.Healthy === true
|
||||
).length
|
||||
}/${deployment.get('desiredTotal')} running and healthy`,
|
||||
'Summary text shows accurate numbers when some are running/healthy'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-old-allocation-tally]').textContent.trim(),
|
||||
`Previous allocations: ${
|
||||
this.job.allocations.filter(
|
||||
(a) =>
|
||||
(a.clientStatus === 'running' || a.clientStatus === 'complete') &&
|
||||
a.jobVersion !== deployment.versionNumber
|
||||
).length
|
||||
} running`,
|
||||
'Old Alloc Summary text shows accurate numbers'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-previous-allocations-legend]')
|
||||
.textContent.trim()
|
||||
.replace(/\s\s+/g, ' '),
|
||||
'25 Running 0 Complete'
|
||||
);
|
||||
|
||||
await percySnapshot(
|
||||
"Job Status Panel: 'New' and 'Previous' allocations, initial deploying state"
|
||||
);
|
||||
|
||||
// Try setting a few of the old allocs to complete and make sure number ticks down
|
||||
await Promise.all(
|
||||
this.get('job.allocations')
|
||||
.filter(
|
||||
(a) =>
|
||||
a.clientStatus === 'running' &&
|
||||
a.jobVersion !== deployment.versionNumber
|
||||
)
|
||||
.slice(0, OLD_ALLOCATIONS_TO_COMPLETE)
|
||||
.map(async (a) => await a.set('clientStatus', 'complete'))
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(
|
||||
'.previous-allocations .allocation-status-row .represented-allocation'
|
||||
)
|
||||
.exists(
|
||||
{ count: OLD_ALLOCATIONS_TO_SHOW },
|
||||
'All old allocations are shown'
|
||||
);
|
||||
assert
|
||||
.dom(
|
||||
'.previous-allocations .allocation-status-row .represented-allocation.complete'
|
||||
)
|
||||
.exists(
|
||||
{ count: OLD_ALLOCATIONS_TO_COMPLETE },
|
||||
'Correct number of old allocations are in completed state'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-old-allocation-tally]').textContent.trim(),
|
||||
`Previous allocations: ${
|
||||
this.job.allocations.filter(
|
||||
(a) =>
|
||||
(a.clientStatus === 'running' || a.clientStatus === 'complete') &&
|
||||
a.jobVersion !== deployment.versionNumber
|
||||
).length - OLD_ALLOCATIONS_TO_COMPLETE
|
||||
} running`,
|
||||
'Old Alloc Summary text shows accurate numbers after some are marked complete'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-previous-allocations-legend]')
|
||||
.textContent.trim()
|
||||
.replace(/\s\s+/g, ' '),
|
||||
'20 Running 5 Complete'
|
||||
);
|
||||
|
||||
await percySnapshot(
|
||||
"Job Status Panel: 'New' and 'Previous' allocations, some old marked complete"
|
||||
);
|
||||
|
||||
await componentA11yAudit(
|
||||
this.element,
|
||||
assert,
|
||||
'scrollable-region-focusable'
|
||||
); //keyframe animation fades from opacity 0
|
||||
});
|
||||
|
||||
test('when there is no running deployment, the latest deployment section shows up for the last deployment', async function (assert) {
|
||||
this.server.create('job', {
|
||||
type: 'service',
|
||||
createAllocations: false,
|
||||
noActiveDeployment: true,
|
||||
});
|
||||
|
||||
await this.store.findAll('job');
|
||||
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
await render(hbs`
|
||||
<JobStatus::Panel @job={{this.job}} />
|
||||
`);
|
||||
|
||||
assert.notOk(find('.active-deployment'), 'No active deployment');
|
||||
assert.ok(
|
||||
find('.running-allocs-title'),
|
||||
'Steady-state mode shown instead'
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -82,6 +82,17 @@ export default create({
|
|||
return this.packStats.toArray().findBy('id', id);
|
||||
},
|
||||
|
||||
statusModes: {
|
||||
current: {
|
||||
scope: '[data-test-status-mode-current]',
|
||||
click: clickable(),
|
||||
},
|
||||
historical: {
|
||||
scope: '[data-test-status-mode-historical]',
|
||||
click: clickable(),
|
||||
},
|
||||
},
|
||||
|
||||
jobClientStatusSummary: {
|
||||
scope: '[data-test-job-client-summary]',
|
||||
statusBar: jobClientStatusBar('[data-test-job-client-status-bar]'),
|
||||
|
@ -93,10 +104,10 @@ export default create({
|
|||
},
|
||||
},
|
||||
childrenSummary: jobClientStatusBar(
|
||||
'[data-test-job-summary] [data-test-children-status-bar]'
|
||||
'[data-test-children-status-bar]:not(.is-narrow)'
|
||||
),
|
||||
allocationsSummary: jobClientStatusBar(
|
||||
'[data-test-job-summary] [data-test-allocation-status-bar]'
|
||||
'[data-test-allocation-status-bar]:not(.is-narrow)'
|
||||
),
|
||||
...taskGroups(),
|
||||
...allocations(),
|
||||
|
|
Loading…
Reference in New Issue