mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
[ui] Jobs list should handle 502s and 504s gracefully (#23427)
* UI handles 502s and 504s gracefully * Test and cleanup
This commit is contained in:
3
.changelog/23427.txt
Normal file
3
.changelog/23427.txt
Normal 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
|
||||
```
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#jobs-list-cache-warning {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status-cell {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -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 don’t 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 don’t 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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user