Merge pull request #17593 from jmchilton/invocation_cleanup

Modernization and cleanup of job state related client code.
This commit is contained in:
Dannon
2024-03-12 06:42:00 -05:00
committed by GitHub
14 changed files with 250 additions and 487 deletions
+7 -5
View File
@@ -2,14 +2,16 @@ import axios from "axios";
import { getAppRoot } from "@/onload";
import { ApiResponse, fetcher } from "./schema";
import { ApiResponse, components, fetcher } from "./schema";
export type WorkflowInvocationElementView = components["schemas"]["WorkflowInvocationElementView"];
export type WorkflowInvocationCollectionView = components["schemas"]["WorkflowInvocationCollectionView"];
export type InvocationJobsSummary = components["schemas"]["InvocationJobsResponse"];
export type InvocationStep = components["schemas"]["InvocationStep"];
export const invocationsFetcher = fetcher.path("/api/invocations").method("get").create();
// TODO: Replace these interfaces with real schema models after https://github.com/galaxyproject/galaxy/pull/16707 is merged
export interface WorkflowInvocation {
id: string;
}
export type WorkflowInvocation = WorkflowInvocationElementView | WorkflowInvocationCollectionView;
export interface WorkflowInvocationJobsSummary {
id: string;
@@ -3,8 +3,8 @@ import CopyToClipboard from "components/CopyToClipboard";
import HelpText from "components/Help/HelpText";
import { JobDetailsProvider } from "components/providers/JobProvider";
import UtcDate from "components/UtcDate";
import { NON_TERMINAL_STATES } from "components/WorkflowInvocationState/util";
import { formatDuration, intervalToDuration } from "date-fns";
import JOB_STATES_MODEL from "utils/job-states-model";
import { computed, ref } from "vue";
import { invocationForJob } from "@/api/invocations";
@@ -30,7 +30,7 @@ const runTime = computed(() =>
formatDuration(intervalToDuration({ start: new Date(job.value.create_time), end: new Date(job.value.update_time) }))
);
const jobIsTerminal = computed(() => job.value && !JOB_STATES_MODEL.NON_TERMINAL_STATES.includes(job.value.state));
const jobIsTerminal = computed(() => job.value && !NON_TERMINAL_STATES.includes(job.value.state));
const routeToInvocation = computed(() => `/workflows/invocations/${invocationId.value}`);
@@ -1,64 +0,0 @@
<template>
<div v-if="!jobSourceType || jobSourceType == 'Job' || (isTerminal && !isErrored)">
{{ simpleDescription }}
</div>
<div v-else-if="!jobStatesSummary || !jobStatesSummary.hasDetails()">
<ProgressBar :note="loadingNote" :loading="true" :info-progress="1" />
</div>
<div v-else-if="isNew">
<ProgressBar note="Creating jobs" :loading="true" :info-progress="1" />
</div>
<div v-else-if="isErrored">
{{ errorDescription }}
</div>
<div v-else>
<ProgressBar
:note="generatingNote"
:ok-count="okCount"
:error-count="errorCount"
:running-count="runningCount"
:new-count="newCount" />
</div>
</template>
<script>
import ProgressBar from "components/ProgressBar";
import DC_VIEW from "mvc/collection/collection-view";
import mixin from "./mixin";
export default {
components: {
ProgressBar,
},
mixins: [mixin],
props: {
collection: { type: Object, required: true }, // backbone model
jobStatesSummary: { required: true },
},
computed: {
loadingNote() {
return `Loading job data for ${this.collectionTypeDescription}`;
},
generatingNote() {
return `${this.jobsStr} generating a ${this.collectionTypeDescription}`;
},
jobSourceType() {
return this.collection.get("job_source_type");
},
collectionTypeDescription() {
return DC_VIEW.collectionTypeDescription(this.collection);
},
simpleDescription() {
return DC_VIEW.collectionDescription(this.collection);
},
errorDescription() {
if (this.isPopulationFailed) {
return `${this.collection.get("populated_state_message")}`;
}
var jobCount = this.jobCount;
var errorCount = this.jobStatesSummary.numInError();
return `a ${this.collectionTypeDescription} with ${errorCount} / ${jobCount} jobs in error`;
},
},
};
</script>
-47
View File
@@ -1,47 +0,0 @@
/* VueJS mixin with computed properties from a base jobStatesSummary property for summarizing job states */
export default {
computed: {
isNew() {
return !this.jobStatesSummary || this.jobStatesSummary.new();
},
isErrored() {
return this.jobStatesSummary && this.jobStatesSummary.errored();
},
isPopulationFailed() {
return this.jobStatesSummary && this.jobStatesSummary.populationFailed();
},
isTerminal() {
return this.jobStatesSummary && this.jobStatesSummary.terminal();
},
jobCount() {
return !this.jobStatesSummary ? null : this.jobStatesSummary.jobCount();
},
jobsStr() {
const jobCount = this.jobCount;
return jobCount && jobCount > 1 ? `${jobCount} jobs` : `a job`;
},
runningCount() {
return this.countStates(["running"]);
},
okCount() {
return this.countStates(["ok", "skipped"]);
},
errorCount() {
return this.countStates(["error", "deleted"]);
},
newCount() {
return this.jobCount - this.okCount - this.runningCount - this.errorCount;
},
},
methods: {
countStates(states) {
let count = 0;
if (this.jobStatesSummary && this.jobStatesSummary.hasDetails()) {
for (const state of states) {
count += this.jobStatesSummary.states()[state] || 0;
}
}
return count;
},
},
};
+3 -3
View File
@@ -1,6 +1,6 @@
import axios from "axios";
import { ERROR_STATES, NON_TERMINAL_STATES } from "components/WorkflowInvocationState/util";
import { getAppRoot } from "onload/loadConfig";
import JOB_STATES_MODEL from "utils/job-states-model";
export function waitOnJob(jobId, onStateUpdate = null, interval = 1000) {
// full=true to capture standard error on last iteration for building
@@ -14,9 +14,9 @@ export function waitOnJob(jobId, onStateUpdate = null, interval = 1000) {
if (onStateUpdate !== null) {
onStateUpdate(state);
}
if (JOB_STATES_MODEL.NON_TERMINAL_STATES.indexOf(state) !== -1) {
if (NON_TERMINAL_STATES.indexOf(state) !== -1) {
setTimeout(checkCondition, interval, resolve, reject);
} else if (JOB_STATES_MODEL.ERROR_STATES.indexOf(state) !== -1) {
} else if (ERROR_STATES.indexOf(state) !== -1) {
reject(jobResponse);
} else {
resolve(jobResponse);
@@ -585,11 +585,11 @@ import SaveRules from "components/RuleBuilder/SaveRules";
import StateDiv from "components/RuleBuilder/StateDiv";
import Select2 from "components/Select2";
import UploadUtils from "components/Upload/utils";
import { ERROR_STATES, NON_TERMINAL_STATES } from "components/WorkflowInvocationState/util";
import $ from "jquery";
import { getAppRoot } from "onload/loadConfig";
import _ from "underscore";
import { refreshContentsWrapper } from "utils/data";
import JobStatesModel from "utils/job-states-model";
import _l from "utils/localization";
import Vue from "vue";
@@ -1337,9 +1337,9 @@ export default {
const handleJobShow = (jobResponse) => {
const state = jobResponse.data.state;
this.waitingJobState = state;
if (JobStatesModel.NON_TERMINAL_STATES.indexOf(state) !== -1) {
if (NON_TERMINAL_STATES.indexOf(state) !== -1) {
setTimeout(doJobCheck, 1000);
} else if (JobStatesModel.ERROR_STATES.indexOf(state) !== -1) {
} else if (ERROR_STATES.indexOf(state) !== -1) {
this.state = "error";
this.errorMessage =
"Unknown error encountered while running your upload job, this could be a server issue or a problem with the upload definition.";
@@ -33,5 +33,6 @@
"input_step_parameters": {},
"outputs": {},
"output_collections": {},
"output_values": {}
"output_values": {},
"messages": []
}
@@ -5,7 +5,6 @@ import { setActivePinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";
import type { WorkflowInvocation } from "@/api/invocations";
import JOB_STATES_MODEL from "@/utils/job-states-model";
import invocationData from "../Workflow/test/json/invocation.json";
@@ -34,7 +33,7 @@ async function mountWorkflowInvocationState(invocation: WorkflowInvocation | nul
},
computed: {
invocation: () => invocation,
jobStatesSummary: () => new JOB_STATES_MODEL.JobStatesSummary(invocationJobsSummaryById),
jobStatesSummary: () => invocationJobsSummaryById,
},
pinia,
localVue,
@@ -45,7 +44,7 @@ async function mountWorkflowInvocationState(invocation: WorkflowInvocation | nul
describe("WorkflowInvocationState.vue", () => {
it("determines that invocation and job states are terminal with terminal invocation", async () => {
const wrapper = await mountWorkflowInvocationState(invocationData);
const wrapper = await mountWorkflowInvocationState(invocationData as WorkflowInvocation);
expect(isInvocationAndJobTerminal(wrapper)).toBe(true);
});
@@ -58,7 +57,7 @@ describe("WorkflowInvocationState.vue", () => {
const invocation = {
...invocationData,
state: "new",
};
} as WorkflowInvocation;
const wrapper = await mountWorkflowInvocationState(invocation);
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);
});
@@ -30,13 +30,12 @@
</b-alert>
</template>
<script>
import mixin from "components/JobStates/mixin";
import LoadingSpan from "components/LoadingSpan";
import JOB_STATES_MODEL from "utils/job-states-model";
import { useInvocationStore } from "@/stores/invocationStore";
import { cancelWorkflowScheduling } from "./services";
import { isTerminal, jobCount } from "./util";
import WorkflowInvocationDetails from "./WorkflowInvocationDetails.vue";
import WorkflowInvocationExportOptions from "./WorkflowInvocationExportOptions.vue";
@@ -49,7 +48,6 @@ export default {
WorkflowInvocationDetails,
WorkflowInvocationExportOptions,
},
mixins: [mixin],
props: {
invocationId: {
type: String,
@@ -91,15 +89,15 @@ export default {
);
},
jobStatesTerminal: function () {
if (this.invocationSchedulingTerminal && this.JobStatesSummary?.jobCount === 0) {
if (this.invocationSchedulingTerminal && jobCount(this.jobStatesSummary) === 0) {
// no jobs for this invocation (think subworkflow or just inputs)
return true;
}
return this.jobStatesSummary && this.jobStatesSummary.terminal();
return this.jobStatesSummary && isTerminal(this.jobStatesSummary);
},
jobStatesSummary() {
const jobsSummary = this.invocationStore.getInvocationJobsSummaryById(this.invocationId);
return !jobsSummary ? null : new JOB_STATES_MODEL.JobStatesSummary(jobsSummary);
return !jobsSummary ? null : jobsSummary;
},
},
created: function () {
@@ -1,3 +1,154 @@
<script setup lang="ts">
import { computed } from "vue";
import { InvocationJobsSummary, InvocationStep, WorkflowInvocationElementView } from "@/api/invocations";
import { getRootFromIndexLink } from "@/onload";
import {
errorCount as jobStatesSummaryErrorCount,
jobCount as jobStatesSummaryJobCount,
numTerminal,
okCount as jobStatesSummaryOkCount,
runningCount as jobStatesSummaryRunningCount,
} from "./util";
import LoadingSpan from "@/components/LoadingSpan.vue";
import ProgressBar from "@/components/ProgressBar.vue";
import InvocationMessage from "@/components/WorkflowInvocationState/InvocationMessage.vue";
function getUrl(path: string): string {
return getRootFromIndexLink() + path;
}
interface Props {
invocation?: WorkflowInvocationElementView;
invocationAndJobTerminal: boolean;
invocationSchedulingTerminal: boolean;
jobStatesSummary: InvocationJobsSummary;
index?: number;
}
const props = defineProps<Props>();
const reportTooltip = "View report for this workflow invocation";
const generatePdfTooltip = "Generate PDF report for this workflow invocation";
const invocationId = computed<string | undefined>(() => props.invocation?.id);
const indexStr = computed(() => {
if (props.index == undefined) {
return "";
} else {
return `${props.index + 1}`;
}
});
const invocationState = computed(() => {
return props.invocation?.state || "new";
});
const invocationStateSuccess = computed(() => {
return invocationState.value == "scheduled" && runningCount.value === 0 && props.invocationAndJobTerminal;
});
const disabledReportTooltip = computed(() => {
const state = invocationState.value;
const runCount = runningCount.value;
if (state != "scheduled") {
return `This workflow is not currently scheduled. The current state is ${state}. Once the workflow is fully scheduled and jobs have complete this option will become available.`;
} else if (runCount != 0) {
return `The workflow invocation still contains ${runCount} running job(s). Once these jobs have completed this option will become available.`;
} else {
return "Steps for this workflow are still running. A report will be available once complete.";
}
});
const stepCount = computed<number>(() => {
return props.invocation?.steps.length || 0;
});
type StepStateType = { [state: string]: number };
const stepStates = computed<StepStateType>(() => {
const stepStates: StepStateType = {};
if (!props.invocation) {
return {};
}
const steps: InvocationStep[] = props.invocation?.steps || [];
for (const step of steps) {
if (!step) {
continue;
}
// the API defined state here allowing null and undefined is odd...
const stepState: string = step.state || "unknown";
if (!stepStates[stepState]) {
stepStates[stepState] = 1;
} else {
stepStates[stepState] += 1;
}
}
return stepStates;
});
const invocationLink = computed<string | null>(() => {
const id = invocationId.value;
if (id) {
return getUrl(`workflows/invocations/report?id=${id}`);
} else {
return null;
}
});
const invocationPdfLink = computed<string | null>(() => {
const id = invocationId.value;
if (id) {
return getUrl(`api/invocations/${id}/report.pdf`);
} else {
return null;
}
});
const stepStatesStr = computed<string>(() => {
return `${stepStates.value?.scheduled || 0} of ${stepCount.value} steps successfully scheduled.`;
});
const okCount = computed<number>(() => {
return jobStatesSummaryOkCount(props.jobStatesSummary);
});
const runningCount = computed<number>(() => {
return jobStatesSummaryRunningCount(props.jobStatesSummary);
});
const jobCount = computed<number>(() => {
return jobStatesSummaryJobCount(props.jobStatesSummary);
});
const errorCount = computed<number>(() => {
return jobStatesSummaryErrorCount(props.jobStatesSummary);
});
const newCount = computed<number>(() => {
return jobCount.value - okCount.value - runningCount.value - errorCount.value;
});
const jobStatesStr = computed(() => {
let jobStr = `${numTerminal(props.jobStatesSummary) || 0} of ${jobCount.value} jobs complete`;
if (!props.invocationSchedulingTerminal) {
jobStr += " (total number of jobs will change until all steps fully scheduled)";
}
return `${jobStr}.`;
});
const emit = defineEmits<{
(e: "invocation-cancelled"): void;
}>();
function onCancel() {
emit("invocation-cancelled");
}
</script>
<template>
<div class="mb-3 workflow-invocation-state-component">
<div v-if="invocationAndJobTerminal">
@@ -71,129 +222,3 @@
class="jobs-progress" />
</div>
</template>
<script>
import mixin from "components/JobStates/mixin";
import LoadingSpan from "components/LoadingSpan";
import ProgressBar from "components/ProgressBar";
import { getRootFromIndexLink } from "onload";
import InvocationMessage from "@/components/WorkflowInvocationState/InvocationMessage.vue";
const getUrl = (path) => getRootFromIndexLink() + path;
export default {
components: {
InvocationMessage,
ProgressBar,
LoadingSpan,
},
mixins: [mixin],
props: {
invocation: {
type: Object,
required: true,
},
invocationAndJobTerminal: {
type: Boolean,
required: true,
},
invocationSchedulingTerminal: {
type: Boolean,
required: true,
},
jobStatesSummary: {
type: Object,
required: false,
default: null,
},
index: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
stepStatesInterval: null,
jobStatesInterval: null,
reportTooltip: "View report for this workflow invocation",
generatePdfTooltip: "Generate PDF report for this workflow invocation",
};
},
computed: {
invocationId() {
return this.invocation?.id;
},
indexStr() {
if (this.index == null) {
return "";
} else {
return `${this.index + 1}`;
}
},
invocationState: function () {
return this.invocation?.state || "new";
},
invocationStateSuccess: function () {
return this.invocationState == "scheduled" && this.runningCount === 0 && this.invocationAndJobTerminal;
},
disabledReportTooltip: function () {
const state = this.invocationState;
const runCount = this.runningCount;
if (this.invocationState != "scheduled") {
return (
"This workflow is not currently scheduled. The current state is ",
state,
". Once the workflow is fully scheduled and jobs have complete this option will become available."
);
} else if (runCount != 0) {
return (
"The workflow invocation still contains ",
runCount,
" running job(s). Once these jobs have completed this option will become available. "
);
} else {
return "Steps for this workflow are still running. A report will be available once complete.";
}
},
stepCount: function () {
return this.invocation?.steps.length;
},
stepStates: function () {
const stepStates = {};
if (!this.invocation) {
return {};
}
for (const step of this.invocation.steps) {
if (!stepStates[step.state]) {
stepStates[step.state] = 1;
} else {
stepStates[step.state] += 1;
}
}
return stepStates;
},
invocationLink: function () {
return getUrl(`workflows/invocations/report?id=${this.invocationId}`);
},
invocationPdfLink: function () {
return getUrl(`api/invocations/${this.invocationId}/report.pdf`);
},
stepStatesStr: function () {
return `${this.stepStates.scheduled || 0} of ${this.stepCount} steps successfully scheduled.`;
},
jobStatesStr: function () {
let jobStr = `${this.jobStatesSummary?.numTerminal() || 0} of ${this.jobCount} jobs complete`;
if (!this.invocationSchedulingTerminal) {
jobStr += " (total number of jobs will change until all steps fully scheduled)";
}
return `${jobStr}.`;
},
},
methods: {
onCancel() {
this.$emit("invocation-cancelled");
},
},
};
</script>
@@ -0,0 +1,71 @@
import { InvocationJobsSummary } from "@/api/invocations";
export const NON_TERMINAL_STATES = ["new", "queued", "running", "waiting"];
export const ERROR_STATES = ["error", "deleted"];
export const TERMINAL_STATES = ["ok", "skipped"].concat(ERROR_STATES);
export const POPULATED_STATE_FAILED = "failed";
function countStates(jobSummary: InvocationJobsSummary | null, queryStates: string[]): number {
let count = 0;
const states = jobSummary?.states;
if (states) {
for (const state of queryStates) {
count += jobSummary.states[state] || 0;
}
}
return count;
}
export function jobCount(jobSummary: InvocationJobsSummary | null) {
const states = jobSummary?.states;
let count = 0;
if (states) {
for (const index in states) {
const stateCount = states[index];
if (stateCount) {
count += stateCount;
}
}
}
return count;
}
export function okCount(jobSummary: InvocationJobsSummary): number {
return countStates(jobSummary, ["ok", "skipped"]);
}
export function runningCount(jobSummary: InvocationJobsSummary): number {
return countStates(jobSummary, ["running"]);
}
export function numTerminal(jobSummary: InvocationJobsSummary): number {
return countStates(jobSummary, TERMINAL_STATES);
}
export function errorCount(jobSummary: InvocationJobsSummary): number {
return countStates(jobSummary, ERROR_STATES);
}
function isNew(jobSummary: InvocationJobsSummary) {
return jobSummary.populated_state && jobSummary.populated_state == "new";
}
function anyWithStates(jobSummary: InvocationJobsSummary, queryStates: string[]) {
const states = jobSummary.states;
for (const index in queryStates) {
const state: string = queryStates[index] as string;
if ((states[state] || 0) > 0) {
return true;
}
}
return false;
}
export function isTerminal(jobSummary: InvocationJobsSummary) {
if (isNew(jobSummary)) {
return false;
} else {
const anyNonTerminal = anyWithStates(jobSummary, NON_TERMINAL_STATES);
return !anyNonTerminal;
}
}
+2 -2
View File
@@ -101,8 +101,8 @@ import JobsTable from "components/admin/JobsTable";
import Heading from "components/Common/Heading";
import filtersMixin from "components/Indices/filtersMixin";
import { jobsProvider } from "components/providers/JobProvider";
import { NON_TERMINAL_STATES } from "components/WorkflowInvocationState/util";
import { getAppRoot } from "onload/loadConfig";
import JOB_STATES_MODEL from "utils/job-states-model";
import { errorMessageAsString } from "utils/simple-error";
import { commonJobFields } from "./JobFields";
@@ -211,7 +211,7 @@ export default {
const unfinishedJobs = [];
const finishedJobs = [];
newVal.forEach((item) => {
if (JOB_STATES_MODEL.NON_TERMINAL_STATES.includes(item.state)) {
if (NON_TERMINAL_STATES.includes(item.state)) {
unfinishedJobs.push(item);
} else {
finishedJobs.push(item);
+2 -2
View File
@@ -1,8 +1,8 @@
import { NON_TERMINAL_STATES } from "components/WorkflowInvocationState/util";
import { snakeCase } from "lodash";
import JOB_STATES_MODEL from "utils/job-states-model";
export function stateIsTerminal(result) {
return !JOB_STATES_MODEL.NON_TERMINAL_STATES.includes(result.state);
return !NON_TERMINAL_STATES.includes(result.state);
}
export const HasAttributesMixin = {
-222
View File
@@ -1,222 +0,0 @@
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import AJAX_QUEUE from "utils/ajax-queue";
/** ms between fetches when checking running jobs/datasets for updates */
var UPDATE_DELAY = 2000;
var NON_TERMINAL_STATES = ["new", "queued", "running", "waiting"];
var ERROR_STATES = ["error", "deleted"];
var TERMINAL_STATES = ["ok", "skipped"].concat(ERROR_STATES);
const POPULATED_STATE_FAILED = "failed";
/** Fetch state on add or just wait for polling to start. */
var FETCH_STATE_ON_ADD = false;
var BATCH_FETCH_STATE = true;
var JobStatesSummary = Backbone.Model.extend({
url: function () {
return `${getAppRoot()}api/histories/${this.attributes.history_id}/contents/dataset_collections/${
this.attributes.collection_id
}/jobs_summary`;
},
hasDetails: function () {
return this.has("populated_state");
},
new: function () {
return !this.hasDetails() || this.get("populated_state") == "new";
},
populationFailed: function () {
return this.get("populated_state") === POPULATED_STATE_FAILED;
},
errored: function () {
return this.populationFailed() || this.anyWithStates(ERROR_STATES);
},
states: function () {
return this.get("states") || {};
},
anyWithState: function (queryState) {
return (this.states()[queryState] || 0) > 0;
},
anyWithStates: function (queryStates) {
var states = this.states();
for (var index in queryStates) {
if ((states[queryStates[index]] || 0) > 0) {
return true;
}
}
return false;
},
numWithStates: function (queryStates) {
var states = this.states();
var count = 0;
for (var index in queryStates) {
count += states[queryStates[index]] || 0;
}
return count;
},
numInError: function () {
return this.numWithStates(ERROR_STATES);
},
numTerminal: function () {
return this.numWithStates(TERMINAL_STATES);
},
running: function () {
return this.anyWithState("running");
},
terminal: function () {
if (this.new()) {
return false;
} else {
var anyNonTerminal = this.anyWithStates(NON_TERMINAL_STATES);
return !anyNonTerminal;
}
},
jobCount: function () {
var states = this.states();
var count = 0;
for (var index in states) {
count += states[index];
}
return count;
},
toString: function () {
return `JobStatesSummary(id=${this.get("id")})`;
},
});
var JobStatesSummaryCollection = Backbone.Collection.extend({
model: JobStatesSummary,
initialize: function () {
/* By default we wait for a polling update to do model fetch because
FETCH_STATE_ON_ADD is false to load the application and target components
as quickly as possible. that said if the polling is turned off
(!this.active) and collections are added - we need to fetch those still.
This happens for instance in the single history view where a history is
shown in a static way and not polled.
*/
if (FETCH_STATE_ON_ADD) {
this.on({
add: (model) => model.fetch(),
});
} else {
this.on({
add: (model) => {
if (!this.active) {
model.fetch();
}
},
});
}
/** cached timeout id for the dataset updater */
this.updateTimeoutId = null;
// this.checkForUpdates();
this.active = true;
},
trackModel: function (historyContent) {
if (historyContent.has("job_states_summary")) {
// already tracked...
return;
}
const historyId = this.historyId;
if (historyContent.attributes.history_content_type === "dataset_collection") {
var jobSourceType = historyContent.attributes.job_source_type;
var jobSourceId = historyContent.attributes.job_source_id;
if (jobSourceType) {
this.add({
id: jobSourceId,
model: jobSourceType,
history_id: historyId,
collection_id: historyContent.attributes.id,
});
historyContent.jobStatesSummary = this.get(jobSourceId);
}
}
},
url: function () {
var nonTerminalModels = this.models.filter((model) => {
return !model.terminal();
});
var ids = nonTerminalModels
.map((summary) => {
return summary.get("id");
})
.join(",");
var types = nonTerminalModels
.map((summary) => {
return summary.get("model");
})
.join(",");
return `${getAppRoot()}api/histories/${this.historyId}/jobs_summary?ids=${ids}&types=${types}`;
},
monitor: function () {
this.clearUpdateTimeout();
if (!this.active) {
return;
}
var _delayThenMonitorAgain = () => {
this.updateTimeoutId = setTimeout(() => {
this.monitor();
}, UPDATE_DELAY);
};
var nonTerminalModels = this.models.filter((model) => {
return !model.terminal();
});
if (nonTerminalModels.length > 0 && !BATCH_FETCH_STATE) {
// Allow models to fetch their own details.
var updateFunctions = nonTerminalModels.map((summary) => {
return () => {
return summary.fetch();
};
});
return new AJAX_QUEUE.AjaxQueue(updateFunctions).done(_delayThenMonitorAgain);
} else if (nonTerminalModels.length > 0) {
// Batch fetch updated state...
this.fetch({ remove: false }).done(_delayThenMonitorAgain);
} else {
_delayThenMonitorAgain();
}
},
/** clear the timeout and the cached timeout id */
clearUpdateTimeout: function () {
if (this.updateTimeoutId) {
clearTimeout(this.updateTimeoutId);
this.updateTimeoutId = null;
}
},
toString: function () {
return `JobStatesSummaryCollection()`;
},
});
export default {
JobStatesSummary,
JobStatesSummaryCollection,
FETCH_STATE_ON_ADD,
NON_TERMINAL_STATES,
ERROR_STATES,
};