mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-11-03 21:34:30 +08:00
Co-authored-by: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com>
268 lines
12 KiB
TypeScript
268 lines
12 KiB
TypeScript
import { type IconDefinition, library } from "@fortawesome/fontawesome-svg-core";
|
|
import {
|
|
faCheckCircle,
|
|
faClock,
|
|
faExclamationTriangle,
|
|
faForward,
|
|
faPause,
|
|
faSpinner,
|
|
faTrash,
|
|
} from "@fortawesome/free-solid-svg-icons";
|
|
import Vue, { computed, type Ref, ref } from "vue";
|
|
|
|
import { stepJobsSummaryFetcher, type StepJobSummary, type WorkflowInvocationElementView } from "@/api/invocations";
|
|
import { isWorkflowInput } from "@/components/Workflow/constants";
|
|
import { fromSimple } from "@/components/Workflow/Editor/modules/model";
|
|
import { getWorkflowFull } from "@/components/Workflow/workflows.services";
|
|
import type { Step } from "@/stores/workflowStepStore";
|
|
import type { Workflow } from "@/stores/workflowStore";
|
|
import { rethrowSimple } from "@/utils/simple-error";
|
|
|
|
import { provideScopedWorkflowStores } from "./workflowStores";
|
|
|
|
export interface GraphStep extends Step {
|
|
state?:
|
|
| "new"
|
|
| "upload"
|
|
| "waiting"
|
|
| "queued"
|
|
| "running"
|
|
| "ok"
|
|
| "error"
|
|
| "deleted"
|
|
| "hidden"
|
|
| "setting_metadata"
|
|
| "paused"
|
|
| "skipped";
|
|
jobs: StepJobSummary["states"];
|
|
headerClass?: Record<string, boolean>;
|
|
headerIcon?: IconDefinition;
|
|
headerIconSpin?: boolean;
|
|
}
|
|
interface InvocationGraph extends Workflow {
|
|
steps: { [index: number]: GraphStep };
|
|
}
|
|
|
|
/** Classes for states' icons */
|
|
export const iconClasses: Record<string, { icon: IconDefinition; spin?: boolean; class?: string }> = {
|
|
ok: { icon: faCheckCircle, class: "text-success" },
|
|
error: { icon: faExclamationTriangle, class: "text-danger" },
|
|
paused: { icon: faPause, class: "text-primary" },
|
|
running: { icon: faSpinner, spin: true },
|
|
new: { icon: faClock },
|
|
waiting: { icon: faClock },
|
|
queued: { icon: faClock },
|
|
deleted: { icon: faTrash, class: "text-danger" },
|
|
skipped: { icon: faForward, class: "text-warning" },
|
|
};
|
|
|
|
/** Only one job needs to be in one of these states for the graph step to be in that state */
|
|
const SINGLE_INSTANCE_STATES = ["error", "running", "paused"];
|
|
/** All jobs need to be in one of these states for the graph step to be in that state */
|
|
const ALL_INSTANCES_STATES = ["deleted", "skipped", "new", "queued"];
|
|
|
|
/** Composable that creates a readonly invocation graph and loads it onto a workflow editor canvas for display.
|
|
* @param invocation - The invocation to display in graph view
|
|
* @param workflowId - The id of the workflow that was invoked
|
|
*/
|
|
export function useInvocationGraph(
|
|
invocation: Ref<WorkflowInvocationElementView>,
|
|
workflowId: string | undefined,
|
|
workflowVersion: number | undefined
|
|
) {
|
|
library.add(faCheckCircle, faClock, faExclamationTriangle, faForward, faPause, faSpinner, faTrash);
|
|
|
|
const steps = ref<{ [index: string]: GraphStep }>({});
|
|
const storeId = computed(() => `invocation-${invocation.value.id}`);
|
|
|
|
/** The full invocation mapped onto the original workflow */
|
|
const invocationGraph = ref<InvocationGraph | null>(null);
|
|
|
|
/** The workflow that was invoked */
|
|
const loadedWorkflow = ref<any>(null);
|
|
|
|
provideScopedWorkflowStores(storeId);
|
|
|
|
async function loadInvocationGraph() {
|
|
try {
|
|
if (!workflowId) {
|
|
throw new Error("Workflow Id is not defined");
|
|
}
|
|
if (workflowVersion === undefined) {
|
|
throw new Error("Workflow Version is not defined");
|
|
}
|
|
|
|
// initialize the original full workflow and invocation graph refs (only on the first load)
|
|
if (!loadedWorkflow.value) {
|
|
loadedWorkflow.value = await getWorkflowFull(workflowId, workflowVersion);
|
|
}
|
|
if (!invocationGraph.value) {
|
|
invocationGraph.value = {
|
|
...loadedWorkflow.value,
|
|
id: storeId.value,
|
|
steps: null,
|
|
};
|
|
}
|
|
|
|
// get the job summary for each step in the invocation
|
|
const { data: stepJobsSummary } = await stepJobsSummaryFetcher({ invocation_id: invocation.value.id });
|
|
|
|
/** The original steps of the workflow */
|
|
const originalSteps: Record<string, Step> = { ...loadedWorkflow.value.steps };
|
|
|
|
// for each step in the workflow, store the state and status of jobs
|
|
for (let i = 0; i < Object.keys(originalSteps).length; i++) {
|
|
/** An invocation graph step */
|
|
const graphStepFromWfStep = { ...originalSteps[i] } as GraphStep;
|
|
|
|
/** The type of the step (subworkflow, input, tool, etc.) */
|
|
let type;
|
|
if (graphStepFromWfStep.type === "subworkflow") {
|
|
type = "subworkflow";
|
|
} else if (isWorkflowInput(graphStepFromWfStep.type)) {
|
|
type = "input";
|
|
}
|
|
|
|
/** The raw invocation step */
|
|
const invocationStep = invocation.value.steps[i];
|
|
|
|
if (type !== "input") {
|
|
// there is an invocation step for this workflow step
|
|
if (invocationStep) {
|
|
/** The `populated_state` for this graph step. (This may or may not be used to
|
|
* derive the `state` for this invocation graph step) */
|
|
let populatedState;
|
|
|
|
if (type === "subworkflow") {
|
|
// if the step is a subworkflow, get the populated state from the invocation step
|
|
populatedState = invocationStep.state || undefined;
|
|
|
|
/* TODO:
|
|
Note that subworkflows are often in the `scheduled` state regardless of whether
|
|
their output is successful or not. One good way to visually show if a subworkflow was
|
|
successful is to set `graphStepFromWfStep.state = subworkflow.output?.state`.
|
|
*/
|
|
}
|
|
|
|
// First, try setting the state of the graph step based on its jobs' states or the populated state
|
|
else {
|
|
/** The step job summary for the invocation step (based on its job id) */
|
|
const invocationStepSummary = stepJobsSummary.find((stepJobSummary: StepJobSummary) => {
|
|
if (stepJobSummary.model === "ImplicitCollectionJobs") {
|
|
return stepJobSummary.id === invocationStep.implicit_collection_jobs_id;
|
|
} else {
|
|
return stepJobSummary.id === invocationStep.job_id;
|
|
}
|
|
});
|
|
|
|
if (invocationStepSummary) {
|
|
// the step is not a subworkflow, get the populated state from the invocation step summary
|
|
populatedState = invocationStepSummary.populated_state;
|
|
|
|
if (invocationStepSummary.states) {
|
|
const statesForThisStep = Object.keys(invocationStepSummary.states);
|
|
// set the state of the graph step based on the job states for this step
|
|
graphStepFromWfStep.state = getStepStateFromJobStates(statesForThisStep);
|
|
}
|
|
// now store the job states for this step in the graph step
|
|
graphStepFromWfStep.jobs = invocationStepSummary.states;
|
|
} else {
|
|
// TODO: There is no summary for this step's `job_id`; what does this mean?
|
|
graphStepFromWfStep.state = "waiting";
|
|
}
|
|
}
|
|
|
|
// If the state still hasn't been set, set it based on the populated state
|
|
if (!graphStepFromWfStep.state) {
|
|
if (populatedState === "scheduled" || populatedState === "ready") {
|
|
graphStepFromWfStep.state = "queued";
|
|
} else if (populatedState === "resubmitted") {
|
|
graphStepFromWfStep.state = "new";
|
|
} else if (populatedState === "failed") {
|
|
graphStepFromWfStep.state = "error";
|
|
} else if (populatedState === "deleting") {
|
|
graphStepFromWfStep.state = "deleted";
|
|
} else if (populatedState && !["stop", "stopped"].includes(populatedState)) {
|
|
graphStepFromWfStep.state = populatedState as GraphStep["state"];
|
|
}
|
|
}
|
|
}
|
|
|
|
// there is no invocation step for this workflow step, it is probably queued
|
|
else {
|
|
graphStepFromWfStep.state = "queued";
|
|
}
|
|
|
|
/** Setting the header class for the graph step */
|
|
graphStepFromWfStep.headerClass = {
|
|
"node-header-invocation": true,
|
|
[`header-${graphStepFromWfStep.state}`]: !!graphStepFromWfStep.state,
|
|
};
|
|
// TODO: maybe a different one for inputs? Currently they have no state either.
|
|
|
|
/** Setting the header icon for the graph step */
|
|
if (graphStepFromWfStep.state) {
|
|
graphStepFromWfStep.headerIcon = iconClasses[graphStepFromWfStep.state]?.icon;
|
|
graphStepFromWfStep.headerIconSpin = iconClasses[graphStepFromWfStep.state]?.spin;
|
|
}
|
|
}
|
|
|
|
// update the invocation graph steps object
|
|
Vue.set(steps.value, i, graphStepFromWfStep);
|
|
}
|
|
|
|
invocationGraph.value!.steps = { ...steps.value };
|
|
|
|
// Load the invocation graph into the editor every time
|
|
await fromSimple(storeId.value, invocationGraph.value as any);
|
|
} catch (e) {
|
|
rethrowSimple(e);
|
|
}
|
|
}
|
|
|
|
/** Given the job states for a step, if the states fall into a single instance state
|
|
* or all instances state, return the state of the step.
|
|
* @param jobStates - The job states for a step
|
|
* @returns The state for the graph step or `undefined` if the states don't match any
|
|
* single instance state or all instances state
|
|
* */
|
|
function getStepStateFromJobStates(jobStates: string[]): GraphStep["state"] | undefined {
|
|
for (const state of SINGLE_INSTANCE_STATES) {
|
|
if (jobStates.includes(state)) {
|
|
return state as GraphStep["state"];
|
|
}
|
|
}
|
|
for (const state of ALL_INSTANCES_STATES) {
|
|
if (jobStates.every((jobState) => jobState === state)) {
|
|
return state as GraphStep["state"];
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// TODO: Maybe we can use this to layout the graph after the steps are loaded (for neatness)?
|
|
// async function layoutGraph() {
|
|
// const newSteps = await autoLayout(storeId.value, steps.value);
|
|
// if (newSteps) {
|
|
// newSteps?.map((step: any) => stepStore.updateStep(step));
|
|
// // Object.assign(steps.value, {...steps.value, ...stepStore.steps});
|
|
// Object.keys(steps.value).forEach((key) => {
|
|
// steps.value[key] = { ...steps.value[key], ...(stepStore.steps[key] as GraphStep) };
|
|
// });
|
|
// }
|
|
// invocationGraph.value!.steps = steps.value;
|
|
// await fromSimple(storeId.value, invocationGraph.value as any);
|
|
// }
|
|
|
|
return {
|
|
/** An id used to scope the store to the invocation's id */
|
|
storeId,
|
|
/** The steps of the invocation graph */
|
|
steps,
|
|
/** Fetches the original workflow structure (once) and the step job summaries for each step in the invocation,
|
|
* and displays the job states on the workflow graph steps.
|
|
*/
|
|
loadInvocationGraph,
|
|
};
|
|
}
|