[ui] Jobs list should handle 502s and 504s gracefully (#23427)

* UI handles 502s and 504s gracefully

* Test and cleanup
This commit is contained in:
Phil Renaud
2024-06-26 21:51:18 -04:00
committed by GitHub
parent 6df8537b69
commit 54aafa574d
6 changed files with 468 additions and 317 deletions

3
.changelog/23427.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:bug
ui: fix an issue where gateway timeouts would cause the jobs list to revert to null, gives users a Pause Fetch option
```

View File

@@ -22,6 +22,7 @@ export default class JobsIndexController extends Controller {
@service store;
@service userSettings;
@service watchList;
@service notifications;
@tracked pageSize;
@@ -156,12 +157,70 @@ export default class JobsIndexController extends Controller {
);
}
/**
* In case the user wants to specifically stop polling for new jobs
*/
@action pauseJobFetching() {
let notification = this.notifications.queue.find(
(n) => n.title === 'Error fetching jobs'
);
if (notification) {
notification.destroyMessage();
}
this.watchList.jobsIndexIDsController.abort();
this.watchList.jobsIndexDetailsController.abort();
this.watchJobIDs.cancelAll();
this.watchJobs.cancelAll();
}
@action restartJobList() {
this.showingCachedJobs = false;
let notification = this.notifications.queue.find(
(n) => n.title === 'Error fetching jobs'
);
if (notification) {
notification.destroyMessage();
}
this.watchList.jobsIndexIDsController.abort();
this.watchList.jobsIndexDetailsController.abort();
this.watchJobIDs.cancelAll();
this.watchJobs.cancelAll();
this.watchJobIDs.perform({}, JOB_LIST_THROTTLE);
this.watchJobs.perform(this.jobIDs, JOB_DETAILS_THROTTLE);
}
@localStorageProperty('nomadLiveUpdateJobsIndex', true) liveUpdatesEnabled;
// #endregion pagination
//#region querying
/**
*
* Let the user know that there was difficulty fetching jobs, but don't overload their screen with notifications.
* Set showingCachedJobs to tell the template to prompt them to extend timeouts
* @param {Error} e
*/
notifyFetchError(e) {
const firstError = e.errors[0];
this.notifications.add({
title: 'Error fetching jobs',
message: `The backend returned an error with status ${firstError.status} while fetching jobs`,
color: 'critical',
sticky: true,
preventDuplicates: true,
});
// Specific check for a proxy timeout error
if (
!this.showingCachedJobs &&
(firstError.status === '502' || firstError.status === '504')
) {
this.showingCachedJobs = true;
}
}
@tracked showingCachedJobs = false;
jobQuery(params) {
this.watchList.jobsIndexIDsController.abort();
this.watchList.jobsIndexIDsController = new AbortController();
@@ -172,9 +231,17 @@ export default class JobsIndexController extends Controller {
abortController: this.watchList.jobsIndexIDsController,
},
})
.then((jobs) => {
this.showingCachedJobs = false;
return jobs;
})
.catch((e) => {
if (e.name !== 'AbortError') {
console.log('error fetching job ids', e);
this.notifyFetchError(e);
}
if (this.jobs.length) {
return this.jobs;
}
return;
});
@@ -194,6 +261,10 @@ export default class JobsIndexController extends Controller {
.catch((e) => {
if (e.name !== 'AbortError') {
console.log('error fetching job allocs', e);
this.notifyFetchError(e);
}
if (this.jobs.length) {
return this.jobs;
}
return;
});
@@ -257,8 +328,14 @@ export default class JobsIndexController extends Controller {
this.pendingJobIDs = jobIDs;
this.pendingJobs = newJobs;
}
if (Ember.testing) {
break;
}
yield timeout(throttle);
} else {
if (Ember.testing) {
break;
}
// This returns undefined on page change / cursorAt change, resulting from the aborting of the old query.
yield timeout(throttle);
this.watchJobs.perform(this.jobIDs, throttle);

View File

@@ -71,7 +71,13 @@ export default class JobSerializer extends ApplicationSerializer {
return super.normalize(typeHash, hash);
}
normalizeQueryResponse(store, primaryModelClass, payload, id, requestType) {
normalizeQueryResponse(
store,
primaryModelClass,
payload = [],
id,
requestType
) {
// What jobs did we ask for?
if (payload._requestBody?.jobs) {
let requestedJobIDs = payload._requestBody.jobs;

View File

@@ -43,6 +43,10 @@
}
}
#jobs-list-cache-warning {
margin-bottom: 1rem;
}
.status-cell {
display: flex;
gap: 0.5rem;

View File

@@ -5,337 +5,349 @@
{{page-title "Jobs"}}
<section class="section">
<Hds::PageHeader id="jobs-list-header" as |PH|>
<PH.Actions id="jobs-list-actions">
{{#if this.showingCachedJobs}}
<Hds::Alert @type="inline" @color="warning" id="jobs-list-cache-warning" as |A|>
<A.Title>Error fetching jobs — shown jobs are cached</A.Title>
<A.Description>Jobs shown are cached and may be out of date. This is often due to a short timeout in proxy configurations.</A.Description>
{{#if this.watchJobIDs.isRunning}}
<A.Button data-test-pause-fetching @text="Stop polling for job updates" @color="secondary" {{on "click" this.pauseJobFetching}} />
{{/if}}
<A.Button data-test-restart-fetching @text="Manually fetch jobs" @color="secondary" {{on "click" this.restartJobList}} />
<A.LinkStandalone @size="medium" @color="primary" @icon="learn-link" @iconPosition="trailing" @text="Tutorial: Configure reverse proxy for Nomad's web UI" @href="https://developer.hashicorp.com/nomad/tutorials/manage-clusters/reverse-proxy-ui#extend-connection-timeout" />
</Hds::Alert>
{{/if}}
<Hds::SegmentedGroup as |S|>
<Hds::PageHeader id="jobs-list-header" as |PH|>
<PH.Actions id="jobs-list-actions">
<JobSearchBox
@searchText={{this.rawSearchText}}
@onSearchTextChange={{queue
this.updateSearchText
this.updateFilter
}}
@S={{S}}
/>
<Hds::SegmentedGroup as |S|>
{{#each this.filterFacets as |group|}}
<S.Dropdown data-test-facet={{group.label}} @height="300px" as |dd|>
<dd.ToggleButton
@text={{group.label}}
@color="secondary"
@count={{get (filter-by "checked" true group.options) "length"}}
/>
{{#each group.options as |option|}}
<dd.Checkbox
{{on "change" (fn this.toggleOption option)}}
@value={{option.key}}
checked={{option.checked}}
data-test-dropdown-option={{option.key}}
>
{{option.key}}
</dd.Checkbox>
{{else}}
<dd.Generic data-test-dropdown-empty>
No {{group.label}} filters
</dd.Generic>
{{/each}}
</S.Dropdown>
{{/each}}
<JobSearchBox
@searchText={{this.rawSearchText}}
@onSearchTextChange={{queue
this.updateSearchText
this.updateFilter
}}
@S={{S}}
/>
{{#if this.system.shouldShowNamespaces}}
<S.Dropdown data-test-facet="Namespace" @height="300px" as |dd|>
<dd.ToggleButton
@text="Namespace"
@color="secondary"
@badge={{get (find-by "checked" true this.namespaceFacet.options) "label"}}
/>
<dd.Header @hasDivider={{true}}>
<Hds::Form::TextInput::Base
@type="search"
placeholder="Filter Namespaces"
@value={{this.namespaceFilter}}
{{autofocus}}
{{on "input" (action this.filterNamespaces )}}
data-test-namespace-filter-searchbox
/>
</dd.Header>
{{#each this.shownNamespaces as |option|}}
<dd.Radio
name={{option.key}}
{{on "change" (fn this.toggleNamespaceOption option dd)}}
@value={{option.key}}
checked={{option.checked}}
data-test-dropdown-option={{option.key}}
>
{{option.label}}
</dd.Radio>
{{/each}}
</S.Dropdown>
{{/if}}
{{#if this.filter}}
<S.Button
@text="Reset Filters"
@color="critical"
@icon="delete"
@size="medium"
{{on "click" (action this.resetFilters)}}
{{#each this.filterFacets as |group|}}
<S.Dropdown data-test-facet={{group.label}} @height="300px" as |dd|>
<dd.ToggleButton
@text={{group.label}}
@color="secondary"
@count={{get (filter-by "checked" true group.options) "length"}}
/>
{{/if}}
{{#each group.options as |option|}}
<dd.Checkbox
{{on "change" (fn this.toggleOption option)}}
@value={{option.key}}
checked={{option.checked}}
data-test-dropdown-option={{option.key}}
>
{{option.key}}
</dd.Checkbox>
{{else}}
<dd.Generic data-test-dropdown-empty>
No {{group.label}} filters
</dd.Generic>
{{/each}}
</S.Dropdown>
{{/each}}
</Hds::SegmentedGroup>
{{#if this.system.shouldShowNamespaces}}
<S.Dropdown data-test-facet="Namespace" @height="300px" as |dd|>
<dd.ToggleButton
@text="Namespace"
@color="secondary"
@badge={{get (find-by "checked" true this.namespaceFacet.options) "label"}}
/>
<dd.Header @hasDivider={{true}}>
<Hds::Form::TextInput::Base
@type="search"
placeholder="Filter Namespaces"
@value={{this.namespaceFilter}}
{{autofocus}}
{{on "input" (action this.filterNamespaces )}}
data-test-namespace-filter-searchbox
/>
</dd.Header>
{{#each this.shownNamespaces as |option|}}
<dd.Radio
name={{option.key}}
{{on "change" (fn this.toggleNamespaceOption option dd)}}
@value={{option.key}}
checked={{option.checked}}
data-test-dropdown-option={{option.key}}
>
{{option.label}}
</dd.Radio>
{{/each}}
</S.Dropdown>
{{/if}}
{{#if this.pendingJobIDDiff}}
<Hds::Button
{{#if this.filter}}
<S.Button
@text="Reset Filters"
@color="critical"
@icon="delete"
@size="medium"
@text="Updates Pending"
@color="primary"
@icon="sync"
{{on "click" (perform this.updateJobList)}}
data-test-updates-pending-button
{{on "click" (action this.resetFilters)}}
/>
{{/if}}
<div
{{keyboard-shortcut
label="Run Job"
pattern=(array "r" "u" "n")
action=(action this.goToRun)
}}
>
<Hds::Button
data-test-run-job
@text="Run Job"
disabled={{not (can "run job")}}
title={{if (can "run job") null "You dont have permission to run jobs"}}
@route="jobs.run"
@query={{hash namespace=this.qpNamespace}}
/>
</div>
</Hds::SegmentedGroup>
</PH.Actions>
</Hds::PageHeader>
{{#if this.pendingJobIDDiff}}
<Hds::Button
@size="medium"
@text="Updates Pending"
@color="primary"
@icon="sync"
{{on "click" (perform this.updateJobList)}}
data-test-updates-pending-button
/>
{{/if}}
{{#if this.isForbidden}}
<ForbiddenMessage />
{{else if this.jobs.length}}
<Hds::Table
@model={{this.jobs}}
@columns={{this.tableColumns}}
@valign="middle"
<div
{{keyboard-shortcut
label="Run Job"
pattern=(array "r" "u" "n")
action=(action this.goToRun)
}}
>
<:body as |B|>
{{!-- TODO: use <JobRow> --}}
<B.Tr
{{keyboard-shortcut
enumerated=true
action=(action "gotoJob" B.data)
}}
{{on "click" (action this.gotoJob B.data)}}
class="job-row is-interactive {{if B.data.assumeGC "assume-gc"}}"
data-test-job-row={{B.data.plainId}}
data-test-modify-index={{B.data.modifyIndex}}
>
{{!-- {{#each this.tableColumns as |column|}}
<B.Td>{{get B.data (lowercase column.label)}}</B.Td>
{{/each}} --}}
<B.Td data-test-job-name>
{{#if B.data.assumeGC}}
{{B.data.name}}
{{else}}
<LinkTo
@route="jobs.job.index"
@model={{B.data.idWithNamespace}}
class="is-primary"
>
{{B.data.name}}
{{!-- TODO: going to lose .meta with statuses endpoint! --}}
{{#if B.data.meta.structured.pack}}
<span data-test-pack-tag class="tag is-pack">
{{x-icon "box" class= "test"}}
<span>Pack</span>
</span>
{{/if}}
</LinkTo>
<Hds::Button
data-test-run-job
@text="Run Job"
disabled={{not (can "run job")}}
title={{if (can "run job") null "You dont have permission to run jobs"}}
@route="jobs.run"
@query={{hash namespace=this.qpNamespace}}
/>
</div>
</PH.Actions>
</Hds::PageHeader>
{{#if this.isForbidden}}
<ForbiddenMessage />
{{else if this.jobs.length}}
<Hds::Table
@model={{this.jobs}}
@columns={{this.tableColumns}}
@valign="middle"
>
<:body as |B|>
{{!-- TODO: use <JobRow> --}}
<B.Tr
{{keyboard-shortcut
enumerated=true
action=(action "gotoJob" B.data)
}}
{{on "click" (action this.gotoJob B.data)}}
class="job-row is-interactive {{if B.data.assumeGC "assume-gc"}}"
data-test-job-row={{B.data.plainId}}
data-test-modify-index={{B.data.modifyIndex}}
>
{{!-- {{#each this.tableColumns as |column|}}
<B.Td>{{get B.data (lowercase column.label)}}</B.Td>
{{/each}} --}}
<B.Td data-test-job-name>
{{#if B.data.assumeGC}}
{{B.data.name}}
{{else}}
<LinkTo
@route="jobs.job.index"
@model={{B.data.idWithNamespace}}
class="is-primary"
>
{{B.data.name}}
{{!-- TODO: going to lose .meta with statuses endpoint! --}}
{{#if B.data.meta.structured.pack}}
<span data-test-pack-tag class="tag is-pack">
{{x-icon "box" class= "test"}}
<span>Pack</span>
</span>
{{/if}}
</B.Td>
{{#if this.system.shouldShowNamespaces}}
<B.Td data-test-job-namespace>{{B.data.namespace.id}}</B.Td>
</LinkTo>
{{/if}}
<B.Td data-test-job-status>
<div class="status-cell">
</B.Td>
{{#if this.system.shouldShowNamespaces}}
<B.Td data-test-job-namespace>{{B.data.namespace.id}}</B.Td>
{{/if}}
<B.Td data-test-job-status>
<div class="status-cell">
{{#if (not (eq B.data.childStatuses null))}}
{{#if B.data.childStatusBreakdown.running}}
<Hds::Badge @icon="corner-down-right" @text="{{B.data.childStatusBreakdown.running}} running {{pluralize "job" B.data.childStatusBreakdown.running}}" @color="highlight" @size="large" />
{{else if B.data.childStatusBreakdown.pending}}
<Hds::Badge @icon="corner-down-right" @text="{{B.data.childStatusBreakdown.pending}} pending {{pluralize "job" B.data.childStatusBreakdown.pending}}" @color="neutral" @size="large" />
{{else if B.data.childStatusBreakdown.dead}}
<Hds::Badge @icon="corner-down-right" @text="{{B.data.childStatusBreakdown.dead}} completed {{pluralize "job" B.data.childStatusBreakdown.dead}}" @color="neutral" @size="large" />
{{else if (not B.data.childStatuses.length)}}
<Hds::Badge @icon="corner-down-right" @text="No child jobs" @color="neutral" @size="large" />
{{/if}}
{{else}}
<Hds::Badge @text="{{capitalize B.data.aggregateAllocStatus.label}}" @color={{B.data.aggregateAllocStatus.state}} @size="large" />
{{/if}}
{{#if B.data.hasPausedTask}}
<Hds::TooltipButton @text="At least one task is paused" @placement="right" data-test-paused-task-indicator>
<Hds::Badge @text="At least one task is paused" @icon="delay" @isIconOnly={{true}} @color="highlight" @size="large" />
</Hds::TooltipButton>
{{/if}}
</div>
</B.Td>
<B.Td data-test-job-type={{B.data.type}}>
{{B.data.type}}
</B.Td>
{{#if this.system.shouldShowNodepools}}
<B.Td data-test-job-node-pool>{{B.data.nodePool}}</B.Td>
{{/if}}
<B.Td>
<div class="job-status-panel compact">
{{#unless B.data.assumeGC}}
{{#if (not (eq B.data.childStatuses null))}}
{{#if B.data.childStatusBreakdown.running}}
<Hds::Badge @icon="corner-down-right" @text="{{B.data.childStatusBreakdown.running}} running {{pluralize "job" B.data.childStatusBreakdown.running}}" @color="highlight" @size="large" />
{{else if B.data.childStatusBreakdown.pending}}
<Hds::Badge @icon="corner-down-right" @text="{{B.data.childStatusBreakdown.pending}} pending {{pluralize "job" B.data.childStatusBreakdown.pending}}" @color="neutral" @size="large" />
{{else if B.data.childStatusBreakdown.dead}}
<Hds::Badge @icon="corner-down-right" @text="{{B.data.childStatusBreakdown.dead}} completed {{pluralize "job" B.data.childStatusBreakdown.dead}}" @color="neutral" @size="large" />
{{else if (not B.data.childStatuses.length)}}
<Hds::Badge @icon="corner-down-right" @text="No child jobs" @color="neutral" @size="large" />
{{#if B.data.childStatuses.length}}
<Hds::Link::Standalone @icon="chevron-right" @text="View {{B.data.childStatuses.length}} child {{pluralize "job" B.data.childStatuses.length}}" @route="jobs.job.index" @model={{B.data.idWithNamespace}} @iconPosition="trailing" />
{{else}}
--
{{/if}}
{{else}}
<Hds::Badge @text="{{capitalize B.data.aggregateAllocStatus.label}}" @color={{B.data.aggregateAllocStatus.state}} @size="large" />
<JobStatus::AllocationStatusRow
@allocBlocks={{B.data.allocBlocks}}
@steady={{true}}
@compact={{true}}
@runningAllocs={{B.data.allocBlocks.running.healthy.nonCanary.length}}
@groupCountSum={{B.data.expectedRunningAllocCount}}
/>
{{/if}}
{{#if B.data.hasPausedTask}}
<Hds::TooltipButton @text="At least one task is paused" @placement="right" data-test-paused-task-indicator>
<Hds::Badge @text="At least one task is paused" @icon="delay" @isIconOnly={{true}} @color="highlight" @size="large" />
</Hds::TooltipButton>
{{/if}}
</div>
</B.Td>
<B.Td data-test-job-type={{B.data.type}}>
{{B.data.type}}
</B.Td>
{{#if this.system.shouldShowNodepools}}
<B.Td data-test-job-node-pool>{{B.data.nodePool}}</B.Td>
{{/if}}
<B.Td>
<div class="job-status-panel compact">
{{#unless B.data.assumeGC}}
{{#if (not (eq B.data.childStatuses null))}}
{{#if B.data.childStatuses.length}}
<Hds::Link::Standalone @icon="chevron-right" @text="View {{B.data.childStatuses.length}} child {{pluralize "job" B.data.childStatuses.length}}" @route="jobs.job.index" @model={{B.data.idWithNamespace}} @iconPosition="trailing" />
{{else}}
--
{{/if}}
{{else}}
<JobStatus::AllocationStatusRow
@allocBlocks={{B.data.allocBlocks}}
@steady={{true}}
@compact={{true}}
@runningAllocs={{B.data.allocBlocks.running.healthy.nonCanary.length}}
@groupCountSum={{B.data.expectedRunningAllocCount}}
/>
{{/if}}
{{/unless}}
</div>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{/unless}}
</div>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<section id="jobs-list-pagination">
<div class="nav-buttons">
<span {{keyboard-shortcut
label="First Page"
pattern=(array "{" "{")
action=(action this.handlePageChange "first")}}
>
<section id="jobs-list-pagination">
<div class="nav-buttons">
<span {{keyboard-shortcut
label="First Page"
pattern=(array "{" "{")
action=(action this.handlePageChange "first")}}
>
<Hds::Button
@text="First"
@color="tertiary"
@size="small"
@icon="chevrons-left"
@iconPosition="leading"
disabled={{not this.cursorAt}}
data-test-pager="first"
{{on "click" (action this.handlePageChange "first")}}
/>
</span>
<span {{keyboard-shortcut
label="Previous Page"
pattern=(array "[" "[")
action=(action this.handlePageChange "prev")}}
>
<Hds::Button
@text="Previous"
@color="tertiary"
@size="small"
@icon="chevron-left"
@iconPosition="leading"
disabled={{not this.cursorAt}}
data-test-pager="previous"
{{on "click" (action this.handlePageChange "prev")}}
/>
</span>
<span {{keyboard-shortcut
label="Next Page"
pattern=(array "]" "]")
action=(action this.handlePageChange "next")}}
>
<Hds::Button
@text="Next"
@color="tertiary"
@size="small"
@icon="chevron-right"
@iconPosition="trailing"
disabled={{not this.nextToken}}
data-test-pager="next"
{{on "click" (action this.handlePageChange "next")}}
/>
</span>
<span {{keyboard-shortcut
label="Last Page"
pattern=(array "}" "}")
action=(action this.handlePageChange "last")}}
>
<Hds::Button
@text="Last"
@color="tertiary"
@icon="chevrons-right"
@iconPosition="trailing"
@size="small"
disabled={{not this.nextToken}}
data-test-pager="last"
{{on "click" (action this.handlePageChange "last")}}
/>
</span>
</div>
<div class="page-size">
<PageSizeSelect @onChange={{this.handlePageSizeChange}} />
</div>
</section>
{{else}}
<Hds::ApplicationState data-test-empty-jobs-list as |A|>
{{#if this.filter}}
<A.Header data-test-empty-jobs-list-headline @title="No Matches" />
<A.Body>
{{this.humanizedFilterError}}
<br><br>
{{#if this.model.error.correction}}
Did you mean
<Hds::Button
@text="First"
data-test-filter-correction
@text={{this.model.error.correction.correctKey}}
@isInline={{true}}
@color="tertiary"
@size="small"
@icon="chevrons-left"
@iconPosition="leading"
disabled={{not this.cursorAt}}
data-test-pager="first"
{{on "click" (action this.handlePageChange "first")}}
/>
</span>
<span {{keyboard-shortcut
label="Previous Page"
pattern=(array "[" "[")
action=(action this.handlePageChange "prev")}}
>
<Hds::Button
@text="Previous"
@color="tertiary"
@size="small"
@icon="chevron-left"
@iconPosition="leading"
disabled={{not this.cursorAt}}
data-test-pager="previous"
{{on "click" (action this.handlePageChange "prev")}}
/>
</span>
<span {{keyboard-shortcut
label="Next Page"
pattern=(array "]" "]")
action=(action this.handlePageChange "next")}}
>
<Hds::Button
@text="Next"
@color="tertiary"
@size="small"
@icon="chevron-right"
@icon="bulb"
@iconPosition="trailing"
disabled={{not this.nextToken}}
data-test-pager="next"
{{on "click" (action this.handlePageChange "next")}}
/>
</span>
<span {{keyboard-shortcut
label="Last Page"
pattern=(array "}" "}")
action=(action this.handlePageChange "last")}}
>
<Hds::Button
@text="Last"
@color="tertiary"
@icon="chevrons-right"
@iconPosition="trailing"
@size="small"
disabled={{not this.nextToken}}
data-test-pager="last"
{{on "click" (action this.handlePageChange "last")}}
/>
</span>
</div>
<div class="page-size">
<PageSizeSelect @onChange={{this.handlePageSizeChange}} />
</div>
</section>
{{else}}
<Hds::ApplicationState data-test-empty-jobs-list as |A|>
{{#if this.filter}}
<A.Header data-test-empty-jobs-list-headline @title="No Matches" />
<A.Body>
{{this.humanizedFilterError}}
<br><br>
{{#if this.model.error.correction}}
Did you mean
<Hds::Button
data-test-filter-correction
@text={{this.model.error.correction.correctKey}}
{{on "click" (action this.correctFilterKey this.model.error.correction)}}
/>?
{{else if this.model.error.suggestion}}
<ul>
{{#each this.model.error.suggestion as |suggestion|}}
<li><Hds::Button
data-test-filter-suggestion
@text={{suggestion.key}}
@isInline={{true}}
@size="small"
@color="tertiary"
@icon="bulb"
@iconPosition="trailing"
{{on "click" (action this.correctFilterKey this.model.error.correction)}}
/>?
{{else if this.model.error.suggestion}}
<ul>
{{#each this.model.error.suggestion as |suggestion|}}
<li><Hds::Button
data-test-filter-suggestion
@text={{suggestion.key}}
@isInline={{true}}
@size="small"
@color="tertiary"
@icon="bulb"
{{on "click" (action this.suggestFilter suggestion)}}
/></li>
{{/each}}
</ul>
{{else}}
{{!-- This is the "Nothing was found for your otherwise valid filter" option. Give them suggestions --}}
Did you know: you can try using filter expressions to search through your jobs.
Try <Hds::Button
data-test-filter-random-suggestion
@text={{this.exampleFilter}}
@isInline={{true}}
@color="tertiary"
@icon="bulb"
@iconPosition="trailing"
{{on "click" (action this.suggestFilter
(hash example=this.exampleFilter)
)}}
/>
{{/if}}
</A.Body>
{{on "click" (action this.suggestFilter suggestion)}}
/></li>
{{/each}}
</ul>
{{else}}
{{!-- This is the "Nothing was found for your otherwise valid filter" option. Give them suggestions --}}
Did you know: you can try using filter expressions to search through your jobs.
Try <Hds::Button
data-test-filter-random-suggestion
@text={{this.exampleFilter}}
@isInline={{true}}
@color="tertiary"
@icon="bulb"
@iconPosition="trailing"
{{on "click" (action this.suggestFilter
(hash example=this.exampleFilter)
)}}
/>
{{/if}}
</A.Body>
<A.Footer @hasDivider={{true}} as |F|>
<Hds::Button
@text="Reset Filters"
@@ -351,16 +363,16 @@
@iconPosition="trailing" />
</A.Footer>
{{else}}
<A.Header data-test-empty-jobs-list-headline @title="No Jobs" />
<A.Body
@text="No jobs found."
/>
<A.Footer @hasDivider={{true}} as |F|>
<F.LinkStandalone @icon="plus" @text="Run a New Job" @route="jobs.run"
@iconPosition="trailing" />
</A.Footer>
{{/if}}
</Hds::ApplicationState>
{{/if}}
{{else}}
<A.Header data-test-empty-jobs-list-headline @title="No Jobs" />
<A.Body
@text="No jobs found."
/>
<A.Footer @hasDivider={{true}} as |F|>
<F.LinkStandalone @icon="plus" @text="Run a New Job" @route="jobs.run"
@iconPosition="trailing" />
</A.Footer>
{{/if}}
</Hds::ApplicationState>
{{/if}}
</section>

View File

@@ -250,6 +250,55 @@ module('Acceptance | jobs list', function (hooks) {
assert.equal(currentURL(), '/settings/tokens');
});
test('when a gateway timeout error occurs, appropriate options are shown', async function (assert) {
// Initial request is fine
await JobsList.visit();
assert.dom('#jobs-list-cache-warning').doesNotExist();
server.pretender.get('/v1/jobs/statuses', () => [
504,
{
errors: [
{
status: '504',
},
],
},
null,
]);
const controller = this.owner.lookup('controller:jobs.index');
let currentParams = {
per_page: 10,
};
await controller.watchJobIDs.perform(currentParams, 0);
// Manually set its "isRunning" attribute for testing purposes
// (existence of one of the buttons depends on blocking query running, which Ember testing doesnt really support)
controller.watchJobIDs.isRunning = true;
await settled();
assert.dom('#jobs-list-cache-warning').exists();
assert
.dom('.flash-message.alert-critical')
.exists('A toast error message pops up.');
await percySnapshot(assert);
await click('[data-test-pause-fetching]');
assert
.dom('.flash-message.alert-critical')
.doesNotExist('Error message removed when fetrching is paused');
assert.dom('#jobs-list-cache-warning').exists('Cache warning remains');
server.pretender.get('/v1/jobs/statuses', () => [200, {}, null]);
await click('[data-test-restart-fetching]');
assert
.dom('#jobs-list-cache-warning')
.doesNotExist('Cache warning removed when fetching is restarted');
});
function typeForJob(job) {
return job.periodic
? 'periodic'