Commit 3f8c354b authored by Ahmed Awan's avatar Ahmed Awan
Browse files

[24.1] Optimize `useInvocationGraph` for Invocation view

This ensures the graph is only loaded onto the editor once (initially). After that, we update the `steps` ref for each step _only when_ the `step_jobs_summary` changes.

Fixes https://github.com/galaxyproject/galaxy/issues/18660
parent cf211c12
Loading
Loading
Loading
Loading
+143 −100
Original line number Diff line number Diff line
@@ -10,7 +10,12 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import Vue, { computed, type Ref, ref } from "vue";

import { stepJobsSummaryFetcher, type StepJobSummary, type WorkflowInvocationElementView } from "@/api/invocations";
import {
    type InvocationStep,
    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";
@@ -73,8 +78,11 @@ export function useInvocationGraph(
    library.add(faCheckCircle, faClock, faExclamationTriangle, faForward, faPause, faSpinner, faTrash);

    const steps = ref<{ [index: string]: GraphStep }>({});
    const stepsPopulated = ref(false);
    const storeId = computed(() => `invocation-${invocation.value.id}`);

    const lastStepsJobsSummary = ref<StepJobSummary[]>([]);

    /** The full invocation mapped onto the original workflow */
    const invocationGraph = ref<InvocationGraph | null>(null);

@@ -105,56 +113,104 @@ export function useInvocationGraph(
            }

            // get the job summary for each step in the invocation
            const { data: stepJobsSummary } = await stepJobsSummaryFetcher({ invocation_id: invocation.value.id });
            const { data: stepsJobsSummary } = await stepJobsSummaryFetcher({ invocation_id: invocation.value.id });

            /** The original steps of the workflow */
            const originalSteps: Record<string, Step> = { ...loadedWorkflow.value.steps };
            // if the steps have not been populated or the job states have changed, update the steps
            // TODO: What if the state of something not in the stepsJobsSummary has changed? (e.g.: subworkflows...)
            if (
                !stepsPopulated.value ||
                JSON.stringify(stepsJobsSummary) !== JSON.stringify(lastStepsJobsSummary.value)
            ) {
                updateSteps(stepsJobsSummary);

            // 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;
                // Load the invocation graph into the editor the first time
                if (!stepsPopulated.value) {
                    invocationGraph.value!.steps = { ...steps.value };
                    await fromSimple(storeId.value, invocationGraph.value as any);
                    stepsPopulated.value = true;
                }
            }
        } catch (e) {
            rethrowSimple(e);
        }
    }

                /** The type of the step (subworkflow, input, tool, etc.) */
                let type;
                if (graphStepFromWfStep.type === "subworkflow") {
                    type = "subworkflow";
                } else if (isWorkflowInput(graphStepFromWfStep.type)) {
                    type = "input";
    /** Update the steps of the invocation graph with the step job summaries, or initialize the steps
     * if they haven't been populated yet.
     * @param stepsJobsSummary - The job summary for each step in the invocation
     * */
    function updateSteps(stepsJobsSummary: StepJobSummary[]) {
        /** Initialize with the original steps of the workflow, else update the existing graph steps */
        const fullSteps: Record<string, Step | GraphStep> = !stepsPopulated.value
            ? { ...loadedWorkflow.value.steps }
            : steps.value;

        // for each step, store the state and status of jobs
        for (let i = 0; i < Object.keys(fullSteps).length; i++) {
            /** An invocation graph step (initialized with the original workflow step) */
            let graphStepFromWfStep;
            if (!steps.value[i]) {
                graphStepFromWfStep = { ...fullSteps[i] } as GraphStep;
            } else {
                graphStepFromWfStep = steps.value[i] as GraphStep;
            }

            /** The raw invocation step */
            const invocationStep = invocation.value.steps[i];

                if (type !== "input") {
            if (!isWorkflowInput(graphStepFromWfStep.type)) {
                let invocationStepSummary: StepJobSummary | undefined;
                if (invocationStep) {
                    invocationStepSummary = stepsJobsSummary.find((stepJobSummary: StepJobSummary) => {
                        if (stepJobSummary.model === "ImplicitCollectionJobs") {
                            return stepJobSummary.id === invocationStep.implicit_collection_jobs_id;
                        } else {
                            return stepJobSummary.id === invocationStep.job_id;
                        }
                    });
                }
                updateStep(graphStepFromWfStep, invocationStep, invocationStepSummary);
            }

            // add the graph step to the steps object if it doesn't exist yet
            if (!steps.value[i]) {
                Vue.set(steps.value, i, graphStepFromWfStep);
            }
        }

        lastStepsJobsSummary.value = stepsJobsSummary;
    }

    /**
     * Store the state and status of jobs for the graph step.
     * @param graphStep - Invocation graph step
     * @param invocationStep - The invocation step for the workflow step
     * @param invocationStepSummary - The step job summary for the invocation step (based on its job id)
     */
    function updateStep(
        graphStep: GraphStep,
        invocationStep: InvocationStep | undefined,
        invocationStepSummary: StepJobSummary | undefined
    ) {
        // 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 (graphStep.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`.
                    successful is to set `graphStep.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;
@@ -162,61 +218,48 @@ export function useInvocationGraph(
                    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);
                        graphStep.state = getStepStateFromJobStates(statesForThisStep);
                    }
                    // now store the job states for this step in the graph step
                                graphStepFromWfStep.jobs = invocationStepSummary.states;
                    graphStep.jobs = invocationStepSummary.states;
                } else {
                    // TODO: There is no summary for this step's `job_id`; what does this mean?
                                graphStepFromWfStep.state = "waiting";
                    graphStep.state = "waiting";
                }
            }

            // If the state still hasn't been set, set it based on the populated state
                        if (!graphStepFromWfStep.state) {
            if (!graphStep.state) {
                if (populatedState === "scheduled" || populatedState === "ready") {
                                graphStepFromWfStep.state = "queued";
                    graphStep.state = "queued";
                } else if (populatedState === "resubmitted") {
                                graphStepFromWfStep.state = "new";
                    graphStep.state = "new";
                } else if (populatedState === "failed") {
                                graphStepFromWfStep.state = "error";
                    graphStep.state = "error";
                } else if (populatedState === "deleting") {
                                graphStepFromWfStep.state = "deleted";
                    graphStep.state = "deleted";
                } else if (populatedState && !["stop", "stopped"].includes(populatedState)) {
                                graphStepFromWfStep.state = populatedState as GraphStep["state"];
                    graphStep.state = populatedState as GraphStep["state"];
                }
            }
        }

        // there is no invocation step for this workflow step, it is probably queued
        else {
                        graphStepFromWfStep.state = "queued";
            graphStep.state = "queued";
        }

        /** Setting the header class for the graph step */
                    graphStepFromWfStep.headerClass = {
        graphStep.headerClass = {
            "node-header-invocation": true,
                        [`header-${graphStepFromWfStep.state}`]: !!graphStepFromWfStep.state,
            [`header-${graphStep.state}`]: !!graphStep.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);
        if (graphStep.state) {
            graphStep.headerIcon = iconClasses[graphStep.state]?.icon;
            graphStep.headerIconSpin = iconClasses[graphStep.state]?.spin;
        }
    }