Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ui] alert a user on the jobs index page when a deploying job requires manual promotion #20279

Draft
wants to merge 99 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
c46e711
api: new /jobs/statuses endpoint
gulducat Jan 4, 2024
30f82b7
make blocking-query block
gulducat Jan 4, 2024
c32d019
add JobSummary.Children.Desired
gulducat Jan 8, 2024
8ba63f5
GroupCountSum in new /statuses
gulducat Jan 10, 2024
4653b7c
/jobs/statuses2 endpoint for the whole /ui/jobs index table
gulducat Jan 10, 2024
209643c
fix a really silly bug for blocking queries
gulducat Jan 11, 2024
ef1f8ee
add Datacenters
gulducat Jan 11, 2024
c98a90e
flat list of jobs
gulducat Jan 11, 2024
a85fe9b
make /statuses the same shape as /statuses2
gulducat Jan 12, 2024
259de6e
/statuses only unblock on changes to the jobs being watched
gulducat Jan 12, 2024
be7583d
add JobVersion to allocs
gulducat Jan 12, 2024
f366cd6
/statuses2 use new index logic too
gulducat Jan 12, 2024
39b29d4
unblock if job goes away (gc)
gulducat Jan 16, 2024
3f882f9
one /jobs/statuses3 to rule them all
gulducat Jan 16, 2024
927ec87
use filters feature of paginator
gulducat Jan 16, 2024
c6579f1
exclude child jobs, add ChildStatuses
gulducat Jan 19, 2024
07c0c9b
probably unnecessary namespace optimization
gulducat Jan 23, 2024
cbc9344
server benchmarking
gulducat Jan 23, 2024
17b7d3d
smart alloc
gulducat Jan 23, 2024
62eba50
enable reverse sort in statuses3
gulducat Jan 23, 2024
6b22618
fix a panic from DeploymentStatus being nil
gulducat Jan 24, 2024
5fc64a0
clean up; statuses3 -> statuses
gulducat Jan 24, 2024
9fa3de8
fine, ok, copywrite header
gulducat Jan 24, 2024
892318a
add NodeID to allocs
gulducat Feb 1, 2024
64cf698
misc tidying and rearranging
gulducat Feb 13, 2024
544b57f
require read-job acl instead of list
gulducat Feb 14, 2024
ea56874
rpc tests
gulducat Mar 13, 2024
a527a53
misc...
gulducat Mar 13, 2024
817e354
copyright test file
gulducat Mar 13, 2024
312c6d0
revert "add JobSummary.Children.Desired"
gulducat Mar 13, 2024
5a4665b
sort jobs by ModifyIndex
gulducat Mar 25, 2024
485f45b
add submit/modify time
gulducat Mar 25, 2024
4447725
internal errors are not bad requests
gulducat Mar 25, 2024
1b1944b
sort by ModifyIndex *only*, and default reverse
gulducat Apr 5, 2024
e125aeb
minimally fix tests (uncomplicated)
gulducat Apr 9, 2024
404c5c5
test: extra uint64 pagination assurance
gulducat Apr 9, 2024
b681e8b
add LatestDeployment
gulducat Apr 9, 2024
f8ccbd0
remove SmartAlloc
gulducat Apr 22, 2024
21baaae
remove vestigial test helper
gulducat Apr 23, 2024
1f593d9
remove separate Jobs RPC type
gulducat Apr 23, 2024
924935a
treat GET/POST the same
gulducat Apr 24, 2024
3e0ab67
set/slice init size optimization
gulducat Apr 29, 2024
869b4fc
test: caller requests nonexistent job
gulducat Apr 30, 2024
2b726dc
Revert "set/slice init size optimization"
gulducat Apr 30, 2024
73440c2
drop ActiveDeploymentID
gulducat Apr 30, 2024
13b1d77
child job changes
gulducat Apr 30, 2024
f999fbf
Healthy *bool and add FollowupEvalID
gulducat May 2, 2024
1e675f8
Hook and latch on the initial index
philrenaud Jan 18, 2024
4c415a3
Serialization and restart of controller and table
philrenaud Jan 19, 2024
3355da2
de-log
philrenaud Jan 19, 2024
a137c3d
allocBlocks reimplemented at job model level
philrenaud Jan 22, 2024
207ea9c
totalAllocs doesnt mean on jobmodel what it did in steady.js
philrenaud Jan 22, 2024
af1d3a7
Hamburgers to sausages
philrenaud Jan 24, 2024
0294e16
Hacky way to bring new jobs back around and parent job handling in li…
philrenaud Jan 24, 2024
0755fec
Getting closer to hook/latch
philrenaud Jan 26, 2024
24cd854
Latch from update on hook from initialize, but fickle
philrenaud Jan 27, 2024
bcb2419
Note on multiple-watch problem
philrenaud Jan 29, 2024
fcdc480
Sensible monday morning comment removal
philrenaud Jan 29, 2024
d586c57
use of abortController to handle transition and reset events
philrenaud Jan 29, 2024
682aba6
Next token will now update when there's an on-page shift
philrenaud Jan 30, 2024
5354cd2
Very rough anti-jostle technique
philrenaud Jan 30, 2024
63847e9
Demoable, now to move things out of route and into controller
philrenaud Jan 31, 2024
7d5dc4f
Into the controller, generally
philrenaud Jan 31, 2024
d96f5a2
Smarter cancellations
philrenaud Feb 1, 2024
bd7ae5e
Reset abortController on index models run, and system/sysbatch jobs n…
philrenaud Feb 1, 2024
f37c22b
Prev Page reverse querying
philrenaud Feb 2, 2024
746f48f
n+1th jobs existing will trigger nextToken/pagination display
philrenaud Feb 12, 2024
51064e9
Start of a GET/POST statuses return
philrenaud Mar 4, 2024
964e38b
Namespace fix
philrenaud Mar 4, 2024
bfb6496
Unblock tests
philrenaud Mar 4, 2024
3720513
Realizing to my small horror that this skipURLModification flag may b…
philrenaud Mar 5, 2024
a4221eb
Lintfix
philrenaud Mar 6, 2024
57998e9
Default liveupdates localStorage setting to true
philrenaud Mar 6, 2024
28df4f4
Pagination and index rethink
philrenaud Mar 12, 2024
da9649f
Big uncoupling of watchable and url-append stuff
philrenaud Mar 18, 2024
2058c73
Testfixes for region, search, and keyboard
philrenaud Mar 20, 2024
d8f97bc
Job row class for test purposes
philrenaud Mar 20, 2024
52fcead
Allocations in test now contain events
philrenaud Mar 20, 2024
e1d6894
Starting on the jobs list tests in earnest
philrenaud Mar 21, 2024
415642e
Forbidden state de-bubbling cleanup
philrenaud Mar 21, 2024
eb695f0
Job list page size fixes
philrenaud Mar 21, 2024
c19a422
Facet/Search/Filter jobs list tests skipped
philrenaud Mar 21, 2024
d1201af
Maybe it's the automatic mirage logging
philrenaud Mar 22, 2024
2b8dea2
Unbreak task unit test
philrenaud Mar 22, 2024
69198f7
Pre-sort sort
philrenaud Mar 22, 2024
b02d33b
styling for jobs list pagination and general PR cleanup
philrenaud Mar 26, 2024
371d61d
moving from Job.ActiveDeploymentID to Job.LatestDeployment.ID
philrenaud Apr 18, 2024
3c5a77a
modifyIndex-based pagination (#20350)
philrenaud Apr 18, 2024
da6fa95
Bugfix: resizing your browser on the new jobs index page would make t…
philrenaud Apr 19, 2024
3350ebc
[ui] Searching and filtering options (#20459)
philrenaud Apr 27, 2024
bd139db
Merge branch 'main' into bff-job-summary-fe-redux-untangle
gulducat May 3, 2024
02dcfe9
drop .go changes; TODO: rebase cleanly from main
gulducat May 3, 2024
243d45c
todo-squashing
philrenaud May 6, 2024
7a63e48
Beginnings of deployment-aware alerting on the jobs index page
philrenaud Apr 3, 2024
09b80eb
ActiveDeployment to LateestDeploymentSummary
philrenaud May 1, 2024
07a72cd
Reconfigured to use latestDeploymentSummary in job-row computed prope…
philrenaud May 1, 2024
fb02dbe
A couple findings about wontReschedule and health status noted. Work …
philrenaud May 2, 2024
b4aa1fe
Small note on error handling
philrenaud May 2, 2024
7f48efa
Correctly using followUpEvaluation
philrenaud May 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions ui/app/adapters/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { base64EncodeString } from 'nomad-ui/utils/encode';
import classic from 'ember-classic-decorator';
import { inject as service } from '@ember/service';
import { getOwner } from '@ember/application';
import { get } from '@ember/object';

@classic
export default class JobAdapter extends WatchableNamespaceIDs {
Expand Down Expand Up @@ -202,4 +203,65 @@ export default class JobAdapter extends WatchableNamespaceIDs {

return wsUrl;
}

// TODO: Handle the in-job-page query for pack meta per https://github.com/hashicorp/nomad/pull/14833
query(store, type, query, snapshotRecordArray, options) {
options = options || {};
options.adapterOptions = options.adapterOptions || {};

const method = get(options, 'adapterOptions.method') || 'GET';
const url = this.urlForQuery(query, type.modelName, method);

let index = query.index || 1;

if (index && index > 1) {
query.index = index;
}

const signal = get(options, 'adapterOptions.abortController.signal');

return this.ajax(url, method, {
signal,
data: query,
}).then((payload) => {
// If there was a request body, append it to the payload
// We can use this in our serializer to maintain returned job order,
// even if one of the requested jobs is not found (has been GC'd) so as
// not to jostle the user's view.
if (query.jobs) {
payload._requestBody = query;
}
return payload;
});
}

handleResponse(status, headers) {
/**
* @type {Object}
*/
const result = super.handleResponse(...arguments);
if (result) {
result.meta = result.meta || {};
if (headers['x-nomad-nexttoken']) {
result.meta.nextToken = headers['x-nomad-nexttoken'];
}
if (headers['x-nomad-index']) {
result.meta.index = headers['x-nomad-index'];
}
}
return result;
}

urlForQuery(query, modelName, method) {
let baseUrl = `/${this.namespace}/jobs/statuses`;
if (method === 'POST' && query.index) {
baseUrl += baseUrl.includes('?') ? '&' : '?';
baseUrl += `index=${query.index}`;
}
if (method === 'POST' && query.jobs) {
baseUrl += baseUrl.includes('?') ? '&' : '?';
baseUrl += 'namespace=*';
}
return baseUrl;
}
}
10 changes: 4 additions & 6 deletions ui/app/adapters/watchable.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ export default class Watchable extends ApplicationAdapter {
//
// It's either this weird side-effecting thing that also requires a change
// to ajaxOptions or overriding ajax completely.
ajax(url, type, options) {
ajax(url, type, options = {}) {
const hasParams = hasNonBlockingQueryParams(options);
if (!hasParams || type !== 'GET') return super.ajax(url, type, options);

const params = { ...options.data };
let params = { ...options?.data };
delete params.index;

// Options data gets appended as query params as part of ajaxOptions.
Expand Down Expand Up @@ -96,6 +95,7 @@ export default class Watchable extends ApplicationAdapter {
additionalParams = {}
) {
const url = this.buildURL(type.modelName, null, null, 'query', query);
const method = get(options, 'adapterOptions.method') || 'GET';
let [urlPath, params] = url.split('?');
params = assign(
queryString.parse(params) || {},
Expand All @@ -105,15 +105,13 @@ export default class Watchable extends ApplicationAdapter {
);

if (get(options, 'adapterOptions.watch')) {
// The intended query without additional blocking query params is used
// to track the appropriate query index.
params.index = this.watchList.getIndexFor(
`${urlPath}?${queryString.stringify(query)}`
);
}

const signal = get(options, 'adapterOptions.abortController.signal');
return this.ajax(urlPath, 'GET', {
return this.ajax(urlPath, method, {
signal,
data: params,
}).then((payload) => {
Expand Down
140 changes: 120 additions & 20 deletions ui/app/components/job-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,139 @@
* SPDX-License-Identifier: BUSL-1.1
*/

import Component from '@ember/component';
// @ts-check

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { lazyClick } from '../helpers/lazy-click';
import {
classNames,
tagName,
attributeBindings,
} from '@ember-decorators/component';
import classic from 'ember-classic-decorator';

@classic
@tagName('tr')
@classNames('job-row', 'is-interactive')
@attributeBindings('data-test-job-row')
import { task } from 'ember-concurrency';

export default class JobRow extends Component {
@service router;
@service store;
@service system;

job = null;
/**
* Promotion of a deployment will error if the canary allocations are not of status "Healthy";
* this function will check for that and disable the promote button if necessary.
* @returns {boolean}
*/
get canariesHealthy() {
const relevantAllocs = this.args.job.allocations.filter(
(a) => !a.isOld && a.isCanary && !a.hasBeenRescheduled
);
return (
relevantAllocs.length &&
relevantAllocs.every((a) => a.clientStatus === 'running' && a.isHealthy)
);
}

/**
* Used to inform the user that an allocation has entered into a perment state of failure:
* That is, it has exhausted its restarts and its reschedules and is in a terminal state.
*/
get someCanariesHaveFailedAndWontReschedule() {
const relevantAllocs = this.args.job.allocations.filter(
(a) => !a.isOld && a.isCanary && !a.hasBeenRescheduled
);

return relevantAllocs.some(
(a) =>
a.clientStatus === 'failed' ||
a.clientStatus === 'lost' ||
a.isUnhealthy
);
}

// eslint-disable-next-line require-yield
@task(function* () {
/**
* @typedef DeploymentSummary
* @property {string} id
* @property {boolean} isActive
* @property {string} jobVersion
* @property {string} status
* @property {string} statusDescription
* @property {boolean} allAutoPromote
* @property {boolean} requiresPromotion
*/
/**
* @type {DeploymentSummary}
*/
let latestDeploymentSummary = this.args.job.latestDeploymentSummary;

// Early return false if we don't have an active deployment
if (!latestDeploymentSummary.isActive) {
return false;
}

// Early return if we our deployment doesn't have any canaries
if (!this.args.job.hasActiveCanaries) {
console.log('!hasActiveCan');
return false;
}

if (latestDeploymentSummary.requiresPromotion) {
console.log('requires promotion, and...');
if (this.canariesHealthy) {
console.log('canaries are healthy.');
return 'canary-promote';
}

if (this.someCanariesHaveFailedAndWontReschedule) {
console.log('some canaries have failed.');
return 'canary-failure';
}
if (latestDeploymentSummary.allAutoPromote) {
console.log(
'This deployment is set to auto-promote; canaries are being checked now'
);
// return "This deployment is set to auto-promote; canaries are being checked now";
return false;
} else {
console.log(
'This deployment requires manual promotion and things are being checked now'
);
// return "This deployment requires manual promotion and things are being checked now";
return false;
}
}
return false;
})
requiresPromotionTask;

@task(function* () {
try {
yield this.args.job.latestDeployment.content.promote(); // TODO: need to do a deployment findRecord here first.
// dont bubble up
return false;
} catch (err) {
// TODO: handle error. add notifications.
console.log('caught error', err);
// this.handleError({
// title: 'Could Not Promote Deployment',
// // description: messageFromAdapterError(err, 'promote deployments'),
// });

// One of independent, parent, or child. Used to customize the template
// based on the relationship of this job to others.
context = 'independent';
// err.errors.forEach((err) => {
// this.notifications.add({
// title: "Could not promote deployment",
// message: err.detail,
// color: 'critical',
// timeout: 8000,
// });
// });
}
})
promote;

click(event) {
lazyClick([this.gotoJob, event]);
get latestDeploymentFailed() {
return this.args.job.latestDeploymentSummary.status === 'failed';
}

@action
gotoJob() {
const { job } = this;
const { job } = this.args;
this.router.transitionTo('jobs.job.index', job.idWithNamespace);
}
}
20 changes: 20 additions & 0 deletions ui/app/components/job-search-box.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<@S.TextInput
@type="search"
@value={{@searchText}}
aria-label="Job Search"
placeholder="Name contains myJob"
@icon="search"
@width="300px"
{{on "input" (action this.updateSearchText)}}
{{keyboard-shortcut
label="Search Jobs"
pattern=(array "Shift+F")
action=(action this.focus)
}}
data-test-jobs-search
/>
39 changes: 39 additions & 0 deletions ui/app/components/job-search-box.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

// @ts-check

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { debounce } from '@ember/runloop';

const DEBOUNCE_MS = 500;

export default class JobSearchBoxComponent extends Component {
@service keyboard;

element = null;

@action
updateSearchText(event) {
debounce(this, this.sendUpdate, event.target.value, DEBOUNCE_MS);
}

sendUpdate(value) {
this.args.onSearchTextChange(value);
}

@action
focus(element) {
element.focus();
// Because the element is an input,
// and the "hide hints" part of our keynav implementation is on keyUp,
// but the focus action happens on keyDown,
// and the keynav explicitly ignores key input while focused in a text input,
// we need to manually hide the hints here.
this.keyboard.displayHints = false;
}
}
3 changes: 3 additions & 0 deletions ui/app/components/job-status/allocation-status-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import Component from '@glimmer/component';

export default class JobStatusAllocationStatusBlockComponent extends Component {
get countToShow() {
if (this.args.compact) {
return 0;
}
const restWidth = 50;
const restGap = 10;
let cts = Math.floor((this.args.width - (restWidth + restGap)) / 42);
Expand Down
11 changes: 9 additions & 2 deletions ui/app/components/job-status/allocation-status-row.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
~}}

<div class="allocation-status-row">
<div class="allocation-status-row {{if @compact "compact"}}">
{{#if this.showSummaries}}
<div class="alloc-status-summaries"
{{did-insert this.captureElement}}
Expand All @@ -16,10 +16,14 @@
<JobStatus::AllocationStatusBlock
@status={{status}}
@health={{health}}

@canary={{canary}}
@steady={{@steady}}
@count={{allocsByCanary.length}}
@width={{compute (action this.calcPerc) allocsByCanary.length}}
@width={{compute
(action this.calcPerc) allocsByCanary.length
}}
@compact={{@compact}}
@allocs={{allocsByCanary}} />
{{/if}}
{{/each-in}}
Expand Down Expand Up @@ -50,5 +54,8 @@
{{/each-in}}
</div>
{{/if}}
{{#if @compact}}
{{@runningAllocs}}/{{@groupCountSum}}
{{/if}}
</div>

Loading
Loading