diff --git a/client/src/api/invocations.ts b/client/src/api/invocations.ts index f4aaf3cf798..88c34695fb8 100644 --- a/client/src/api/invocations.ts +++ b/client/src/api/invocations.ts @@ -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; diff --git a/client/src/components/JobInformation/JobInformation.vue b/client/src/components/JobInformation/JobInformation.vue index 759e59b9741..085cf6fec0b 100644 --- a/client/src/components/JobInformation/JobInformation.vue +++ b/client/src/components/JobInformation/JobInformation.vue @@ -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}`); diff --git a/client/src/components/JobStates/CollectionJobStates.vue b/client/src/components/JobStates/CollectionJobStates.vue deleted file mode 100644 index f279125648d..00000000000 --- a/client/src/components/JobStates/CollectionJobStates.vue +++ /dev/null @@ -1,64 +0,0 @@ - - diff --git a/client/src/components/JobStates/mixin.js b/client/src/components/JobStates/mixin.js deleted file mode 100644 index f40ad8d5e52..00000000000 --- a/client/src/components/JobStates/mixin.js +++ /dev/null @@ -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; - }, - }, -}; diff --git a/client/src/components/JobStates/wait.js b/client/src/components/JobStates/wait.js index adae902a84c..956d44d41cb 100644 --- a/client/src/components/JobStates/wait.js +++ b/client/src/components/JobStates/wait.js @@ -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); diff --git a/client/src/components/RuleCollectionBuilder.vue b/client/src/components/RuleCollectionBuilder.vue index ba9e57ea742..2b452512a9d 100644 --- a/client/src/components/RuleCollectionBuilder.vue +++ b/client/src/components/RuleCollectionBuilder.vue @@ -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."; diff --git a/client/src/components/Workflow/test/json/invocation.json b/client/src/components/Workflow/test/json/invocation.json index d417f6ce475..5cfb53fc9df 100644 --- a/client/src/components/Workflow/test/json/invocation.json +++ b/client/src/components/Workflow/test/json/invocation.json @@ -33,5 +33,6 @@ "input_step_parameters": {}, "outputs": {}, "output_collections": {}, - "output_values": {} + "output_values": {}, + "messages": [] } \ No newline at end of file diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.test.ts b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.test.ts index cae244c78bd..751bdc5dd32 100644 --- a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.test.ts +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.test.ts @@ -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); }); diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue index 3230d485af2..2b734f4bcfb 100644 --- a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue @@ -30,13 +30,12 @@ + - diff --git a/client/src/components/WorkflowInvocationState/util.ts b/client/src/components/WorkflowInvocationState/util.ts new file mode 100644 index 00000000000..bbda2253e93 --- /dev/null +++ b/client/src/components/WorkflowInvocationState/util.ts @@ -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; + } +} diff --git a/client/src/components/admin/JobsList.vue b/client/src/components/admin/JobsList.vue index 25045f32832..98a3a782874 100644 --- a/client/src/components/admin/JobsList.vue +++ b/client/src/components/admin/JobsList.vue @@ -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); diff --git a/client/src/components/providers/utils.js b/client/src/components/providers/utils.js index 04d0626ace8..7ac65b87c03 100644 --- a/client/src/components/providers/utils.js +++ b/client/src/components/providers/utils.js @@ -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 = { diff --git a/client/src/utils/job-states-model.js b/client/src/utils/job-states-model.js deleted file mode 100644 index 09fa58c9c43..00000000000 --- a/client/src/utils/job-states-model.js +++ /dev/null @@ -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, -};