Skip to content

Commit

Permalink
Merge branch 'release_24.1' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Aug 8, 2024
2 parents 5f635a3 + 35ebfd5 commit 0d1f74e
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 116 deletions.
10 changes: 6 additions & 4 deletions client/src/components/Workflow/Editor/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@
<div class="editor-top-bar" unselectable="on">
<span>
<span class="sr-only">Workflow Editor</span>
<span v-if="!isNewTempWorkflow || name" class="editor-title" :title="name">{{ name }}</span>
<i v-else class="editor-title">Create New Workflow</i>
<span class="editor-title" :title="name"
>{{ name }}
<i v-if="hasChanges" class="text-muted"> (unsaved changes) </i>
</span>
</span>

<b-button-group>
Expand Down Expand Up @@ -540,12 +542,12 @@ export default {
}
},
annotation(newAnnotation, oldAnnotation) {
if (newAnnotation != oldAnnotation && !this.isNewTempWorkflow) {
if (newAnnotation != oldAnnotation) {
this.hasChanges = true;
}
},
name(newName, oldName) {
if (newName != oldName && !this.isNewTempWorkflow) {
if (newName != oldName) {
this.hasChanges = true;
}
},
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Workflow/Editor/Options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const { confirm } = useConfirmDialog();
const saveHover = computed(() => {
if (props.isNewTempWorkflow) {
return "Create a new workflow";
return "Save Workflow";
} else if (!props.hasChanges) {
return "Workflow has no changes";
} else if (props.hasInvalidConnections) {
Expand Down Expand Up @@ -100,7 +100,7 @@ async function onSave() {
<BButton
id="workflow-save-button"
role="button"
variant="link"
:variant="isNewTempWorkflow ? 'primary' : 'link'"
aria-label="Save Workflow"
class="editor-button-save"
:disabled="!isNewTempWorkflow && !hasChanges"
Expand Down
255 changes: 154 additions & 101 deletions client/src/composables/useInvocationGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import {
faSpinner,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import Vue, { computed, type Ref, ref } from "vue";
import { computed, type Ref, ref, set } 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";
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -105,118 +113,163 @@ export function useInvocationGraph(
}

// 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";
const { data: stepsJobsSummary } = await stepJobsSummaryFetcher({ invocation_id: invocation.value.id });

// 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);

// 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 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`.
*/
}
/** 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;
}

// 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";
}
/** The raw invocation step */
const invocationStep = invocation.value.steps[i];

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);
}

// 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"];
}
}
}
// add the graph step to the steps object if it doesn't exist yet
if (!steps.value[i]) {
set(steps.value, i, graphStepFromWfStep);
}
}

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

/** 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;
/**
* Store the state, jobs and class for the graph step based on the invocation step and its job summary.
* @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
) {
/** The new state for the graph step */
let newState = graphStep.state;

// 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 (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 `graphStep.state = subworkflow.output?.state`.
*/
}

// First, try setting the state of the graph step based on its jobs' states or the populated state
else {
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
newState = getStepStateFromJobStates(statesForThisStep);
}
// now store the job states for this step in the graph step, if they changed since the last time
if (JSON.stringify(graphStep.jobs) !== JSON.stringify(invocationStepSummary.states)) {
set(graphStep, "jobs", invocationStepSummary.states);
}
} else {
// TODO: There is no summary for this step's `job_id`; what does this mean?
newState = "waiting";
}
}

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

invocationGraph.value!.steps = { ...steps.value };
// there is no invocation step for this workflow step, it is probably queued
else {
newState = "queued";
}

// Load the invocation graph into the editor every time
await fromSimple(storeId.value, invocationGraph.value as any);
} catch (e) {
rethrowSimple(e);
// if the state has changed, update the graph step
if (graphStep.state !== newState) {
graphStep.state = newState;

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

Expand Down
16 changes: 15 additions & 1 deletion client/src/entry/analysis/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import Modal from "mvc/ui/ui-modal";
import { getAppRoot } from "onload";
import { storeToRefs } from "pinia";
import { ref, watch } from "vue";
import { useRoute } from "vue-router/composables";
import short from "@/components/plugins/short";
import { useRouteQueryBool } from "@/composables/route";
Expand Down Expand Up @@ -113,7 +114,21 @@ export default {
{ immediate: true }
);
const confirmation = ref(null);
const route = useRoute();
watch(
() => route.fullPath,
(newVal, oldVal) => {
// sometimes, the confirmation is not cleared when the route changes
// and the confirmation alert is shown needlessly
if (confirmation.value) {
confirmation.value = null;
}
}
);
return {
confirmation,
toastRef,
confirmDialogRef,
uploadModal,
Expand All @@ -125,7 +140,6 @@ export default {
data() {
return {
config: getGalaxyInstance().config,
confirmation: null,
resendUrl: `${getAppRoot()}user/resend_verification`,
windowManager: null,
};
Expand Down
1 change: 1 addition & 0 deletions lib/galaxy/webapps/galaxy/controllers/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ def build_from_current_history(
# Optionally target a different history than the current one.
history = self.history_manager.get_owned(self.decode_id(history_id), trans.user, current_history=history)
if not user:
trans.response.status = 403
return trans.show_error_message("Must be logged in to create workflows")
if (job_ids is None and dataset_ids is None) or workflow_name is None:
jobs, warnings = summarize(trans, history)
Expand Down
Loading

0 comments on commit 0d1f74e

Please sign in to comment.