Skip to content

Commit

Permalink
[ui] Job Deployment Status: History and Update Props (#16518)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
philrenaud committed Mar 27, 2023
1 parent 64c315c commit 734c560
Show file tree
Hide file tree
Showing 12 changed files with 445 additions and 14 deletions.
53 changes: 53 additions & 0 deletions ui/app/components/job-status/deployment-history.hbs
Original file line number Diff line number Diff line change
@@ -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>
96 changes: 96 additions & 0 deletions ui/app/components/job-status/deployment-history.js
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 8 additions & 5 deletions ui/app/components/job-status/panel.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
{{/if}}


<footer>
<div class="sub-panels">
{{#if this.isActivelyDeploying}}
<legend>
{{#each this.allocTypes as |type|}}
Expand Down Expand Up @@ -104,11 +104,14 @@
</ul>
</section>
{{/if}}
</footer>



</div>

{{#if this.isActivelyDeploying}}
<div class="active-deployment-info-panels">
<JobStatus::DeploymentHistory @deployment={{@job.latestDeployment}} />
<JobStatus::UpdateParams @job={{@job}} />
</div>
{{/if}}
{{/if}}
</div>
</div>
36 changes: 36 additions & 0 deletions ui/app/components/job-status/update-params.hbs
Original file line number Diff line number Diff line change
@@ -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>
67 changes: 67 additions & 0 deletions ui/app/components/job-status/update-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// @ts-check
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';

/**
* @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
*/

export default class JobStatusUpdateParamsComponent extends Component {
@service notifications;

/**
* @type {JobDefinition}
*/
@tracked rawDefinition = null;

get updateParamGroups() {
if (this.rawDefinition) {
return this.rawDefinition.TaskGroups.map((tg) => {
return {
name: tg.Name,
update: tg.Update,
};
});
} else {
return null;
}
}

@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();
}
}
97 changes: 96 additions & 1 deletion ui/app/styles/components/job-status-panel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
}
}

& > .boxed-section-body > footer {
& > .boxed-section-body > .sub-panels {
display: grid;
gap: 0.5rem;
grid-template-columns: 50% 50%;
Expand Down Expand Up @@ -169,4 +169,99 @@
}
}
}

.active-deployment-info-panels {
display: grid;
grid-template-columns: 70% auto;
gap: 1rem;
margin-top: 2rem; // TODO: grid-ify the deployment panel generally and just use gap for this
}

.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);
}
}
Loading

0 comments on commit 734c560

Please sign in to comment.