mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #17593 from jmchilton/invocation_cleanup
Modernization and cleanup of job state related client code.
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user