From 6bf32b5cd28a9cebba39cc85eb803a5889c12b6e Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Mon, 29 Jul 2024 16:07:48 -0500 Subject: [PATCH 01/20] Workflow Invocation view improvements This first commit tackles point A in the roadmap: List of invocations is now not visible continously, it goes away after you settle on one invocation and `mouseleave` the panel area. Roadmap: https://hackmd.io/@nekrut/HkinoEh8A --- .../src/components/Panels/InvocationsPanel.vue | 17 +++++++++++++++-- .../Invocation/InvocationScrollList.vue | 3 +++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/client/src/components/Panels/InvocationsPanel.vue b/client/src/components/Panels/InvocationsPanel.vue index cd329d1a2ae1..ba810258676e 100644 --- a/client/src/components/Panels/InvocationsPanel.vue +++ b/client/src/components/Panels/InvocationsPanel.vue @@ -1,6 +1,7 @@ diff --git a/client/src/components/Workflow/Invocation/InvocationScrollList.vue b/client/src/components/Workflow/Invocation/InvocationScrollList.vue index 0e78c686650f..f42dd3b858ed 100644 --- a/client/src/components/Workflow/Invocation/InvocationScrollList.vue +++ b/client/src/components/Workflow/Invocation/InvocationScrollList.vue @@ -32,6 +32,8 @@ const props = withDefaults(defineProps(), { limit: 20, }); +const emit = defineEmits(["invocation-clicked"]); + library.add(faEye, faArrowDown, faInfoCircle); const stateClasses: Record = { @@ -97,6 +99,7 @@ function cardClicked(invocation: WorkflowInvocation) { let path = `/workflows/invocations/${invocation.id}`; if (props.inPanel) { path += "?from_panel=true"; + emit("invocation-clicked"); } router.push(path); } From b1ddf4faed01ed37d49cd9c167203d269e7c84dc Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Wed, 31 Jul 2024 12:01:09 -0500 Subject: [PATCH 02/20] move invocation steps to a separate tab, show whole step below - Instead of showing steps right next to the invocation graph, moves steps (back) into a separate tab - To make this work, adds a `graphStepsByStoreId` to the `invocationStore` that helps access the graph steps in multiple components (TODO: improve how this is done) - Instead of clicking on a node/step in the graph and showing the job for the step that the user clicks, now we show the whole expanded step below --- .../Workflow/Editor/NodeInvocationText.vue | 7 +- .../Invocation/Graph/InvocationGraph.vue | 153 +++++------------- .../Graph/WorkflowInvocationSteps.vue | 36 +++-- .../WorkflowInvocationState.vue | 11 ++ .../WorkflowInvocationStep.vue | 44 ++--- .../WorkflowInvocationStepHeader.vue | 56 +++++++ client/src/composables/useInvocationGraph.ts | 13 ++ client/src/stores/invocationStore.ts | 7 + client/src/stores/workflowStepStore.ts | 2 + client/src/stores/workflowStore.ts | 1 + 10 files changed, 163 insertions(+), 167 deletions(-) create mode 100644 client/src/components/WorkflowInvocationState/WorkflowInvocationStepHeader.vue diff --git a/client/src/components/Workflow/Editor/NodeInvocationText.vue b/client/src/components/Workflow/Editor/NodeInvocationText.vue index acc3dd31f555..e9e92f2a7b9c 100644 --- a/client/src/components/Workflow/Editor/NodeInvocationText.vue +++ b/client/src/components/Workflow/Editor/NodeInvocationText.vue @@ -2,16 +2,11 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { isWorkflowInput } from "@/components/Workflow/constants"; -import { type GraphStep, iconClasses } from "@/composables/useInvocationGraph"; +import { type GraphStep, iconClasses, statePlaceholders } from "@/composables/useInvocationGraph"; const props = defineProps<{ invocationStep: GraphStep; }>(); - -const statePlaceholders: Record = { - ok: "successful", - error: "failed", -}; + + diff --git a/client/src/composables/useInvocationGraph.ts b/client/src/composables/useInvocationGraph.ts index 59e83a8831eb..89e3570ce459 100644 --- a/client/src/composables/useInvocationGraph.ts +++ b/client/src/composables/useInvocationGraph.ts @@ -12,7 +12,10 @@ import { storeToRefs } from "pinia"; import { computed, type Ref, ref, set } from "vue"; import { GalaxyApi } from "@/api"; +import { fetchCollectionDetails } from "@/api/datasetCollections"; +import { fetchDatasetDetails } from "@/api/datasets"; import { type InvocationStep, type StepJobSummary, type WorkflowInvocationElementView } from "@/api/invocations"; +import { getContentItemState } from "@/components/History/Content/model/states"; import { isWorkflowInput } from "@/components/Workflow/constants"; import { fromSimple } from "@/components/Workflow/Editor/modules/model"; import { getWorkflowFull } from "@/components/Workflow/workflows.services"; @@ -41,6 +44,7 @@ export interface GraphStep extends Step { headerClass?: Record; headerIcon?: IconDefinition; headerIconSpin?: boolean; + nodeText?: string | boolean; } interface InvocationGraph extends Workflow { steps: { [index: number]: GraphStep }; @@ -129,20 +133,13 @@ export function useInvocationGraph( rethrowSimple(error); } - // 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; - } + await 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); @@ -153,7 +150,7 @@ export function useInvocationGraph( * if they haven't been populated yet. * @param stepsJobsSummary - The job summary for each step in the invocation * */ - function updateSteps(stepsJobsSummary: StepJobSummary[]) { + async function updateSteps(stepsJobsSummary: StepJobSummary[]) { /** Initialize with the original steps of the workflow, else update the existing graph steps */ const fullSteps: Record = !stepsPopulated.value ? { ...loadedWorkflow.value.steps } @@ -172,7 +169,13 @@ export function useInvocationGraph( /** The raw invocation step */ const invocationStep = invocation.value.steps[i]; - if (!isWorkflowInput(graphStepFromWfStep.type)) { + // TODO: What if the state of something not in the stepsJobsSummary has changed? (e.g.: subworkflows...) + /** if the steps have not been populated or the job states have changed, update the step */ + const updateNonInputStep = + !stepsPopulated.value || + JSON.stringify(stepsJobsSummary) !== JSON.stringify(lastStepsJobsSummary.value); + + if (updateNonInputStep && !isWorkflowInput(graphStepFromWfStep.type)) { let invocationStepSummary: StepJobSummary | undefined; if (invocationStep) { invocationStepSummary = stepsJobsSummary.find((stepJobSummary: StepJobSummary) => { @@ -184,6 +187,8 @@ export function useInvocationGraph( }); } updateStep(graphStepFromWfStep, invocationStep, invocationStepSummary); + } else if (invocationStep && graphStepFromWfStep.nodeText === undefined) { + await initializeGraphInput(graphStepFromWfStep, invocationStep); } // add the graph step to the steps object if it doesn't exist yet @@ -275,16 +280,7 @@ export function useInvocationGraph( // 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 = getHeaderClass(graphStep.state as string); - // 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; - } + setHeaderClass(graphStep); } } @@ -308,19 +304,47 @@ export function useInvocationGraph( 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); - // } + function setHeaderClass(graphStep: GraphStep) { + /** Setting the header class for the graph step */ + graphStep.headerClass = getHeaderClass(graphStep.state as string); + + /** Setting the header icon for the graph step */ + if (graphStep.state) { + graphStep.headerIcon = iconClasses[graphStep.state]?.icon; + graphStep.headerIconSpin = iconClasses[graphStep.state]?.spin; + } + } + + async function initializeGraphInput(graphStep: GraphStep, invocationStep: InvocationStep) { + const inputItem = invocation.value.inputs[graphStep.id]; + const inputParam = getWorkflowInputParam(invocation.value, invocationStep); + if (inputItem && inputItem?.id !== undefined && inputItem?.id !== null) { + if (inputItem.src === "hda") { + const hda = await fetchDatasetDetails({ id: inputItem.id }); + // TODO: There is a type mismatch for `hda.state` and `GraphStep["state"]` + set(graphStep, "state", getContentItemState(hda)); + set(graphStep, "nodeText", `${hda.hid}: ${hda.name}`); + } else { + const hdca = await fetchCollectionDetails({ id: inputItem.id }); + // TODO: Same type mismatch as above + set(graphStep, "state", getContentItemState(hdca)); + set(graphStep, "nodeText", `${hdca.hid}: ${hdca.name}`); + } + } else if (inputParam) { + if (typeof inputParam.parameter_value === "boolean") { + set(graphStep, "nodeText", inputParam.parameter_value); + } else { + set(graphStep, "nodeText", `${inputParam.parameter_value}`); + } + } + setHeaderClass(graphStep); + } + + function getWorkflowInputParam(invocation: WorkflowInvocationElementView, invocationStep: InvocationStep) { + return Object.values(invocation.input_step_parameters).find( + (param) => param.workflow_step_id === invocationStep.workflow_step_id + ); + } return { /** An id used to scope the store to the invocation's id */ From c87cd3ad9b5d5c1fe8d20639e055ff7405e3dde2 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Fri, 20 Sep 2024 18:04:31 -0500 Subject: [PATCH 14/20] move progress bars to header, shorten `WorkflowRunSuccess` - The progress bars are in `WorkflowInvocationHeader` now, giving more space to the step header being visible below the graph - `WorkflowRunSuccess` is a one line header (refactored), and it uses `GridInvocation` to show batch invocations. --- client/src/components/Grid/GridInvocation.vue | 16 +- .../Grid/configs/invocationsBatch.ts | 80 +++++++ .../Workflow/Run/WorkflowRunSuccess.vue | 141 +++++------- .../WorkflowInvocationHeader.vue | 140 +++++++++++- .../WorkflowInvocationOverview.vue | 202 +----------------- .../WorkflowInvocationState.vue | 71 +++++- 6 files changed, 351 insertions(+), 299 deletions(-) create mode 100644 client/src/components/Grid/configs/invocationsBatch.ts diff --git a/client/src/components/Grid/GridInvocation.vue b/client/src/components/Grid/GridInvocation.vue index d8997096096f..847127cfa0b7 100644 --- a/client/src/components/Grid/GridInvocation.vue +++ b/client/src/components/Grid/GridInvocation.vue @@ -2,7 +2,9 @@ import { storeToRefs } from "pinia"; import { computed } from "vue"; +import type { WorkflowInvocation } from "@/api/invocations"; import invocationsGridConfig from "@/components/Grid/configs/invocations"; +import invocationsBatchConfig from "@/components/Grid/configs/invocationsBatch"; import invocationsHistoryGridConfig from "@/components/Grid/configs/invocationsHistory"; import invocationsWorkflowGridConfig from "@/components/Grid/configs/invocationsWorkflow"; import { useUserStore } from "@/stores/userStore"; @@ -19,6 +21,7 @@ interface Props { headerMessage?: string; ownerGrid?: boolean; filteredFor?: { type: "History" | "StoredWorkflow"; id: string; name: string }; + invocationsList?: WorkflowInvocation[]; } const props = withDefaults(defineProps(), { @@ -26,12 +29,14 @@ const props = withDefaults(defineProps(), { headerMessage: "", ownerGrid: true, filteredFor: undefined, + invocationsList: undefined, }); const { currentUser } = storeToRefs(useUserStore()); const forStoredWorkflow = computed(() => props.filteredFor?.type === "StoredWorkflow"); const forHistory = computed(() => props.filteredFor?.type === "History"); +const forBatch = computed(() => !!props.invocationsList?.length); const effectiveNoInvocationsMessage = computed(() => { let message = props.noInvocationsMessage; @@ -51,6 +56,9 @@ const effectiveTitle = computed(() => { }); const extraProps = computed(() => { + if (forBatch.value) { + return Object.fromEntries(props.invocationsList.map((invocation) => [invocation.id, invocation])); + } const params: { workflow_id?: string; history_id?: string; @@ -72,7 +80,9 @@ const extraProps = computed(() => { }); let gridConfig: GridConfig; -if (forStoredWorkflow.value) { +if (forBatch.value) { + gridConfig = invocationsBatchConfig; +} else if (forStoredWorkflow.value) { gridConfig = invocationsWorkflowGridConfig; } else if (forHistory.value) { gridConfig = invocationsHistoryGridConfig; @@ -97,9 +107,9 @@ function refreshTable() { :grid-message="props.headerMessage" :no-data-message="effectiveNoInvocationsMessage" :extra-props="extraProps" - :embedded="forStoredWorkflow || forHistory"> + :embedded="forStoredWorkflow || forHistory || forBatch">