From a24486ad4e31d45a22a47b2b1868487eeec89dcc Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 5 Dec 2024 10:24:41 -0600 Subject: [PATCH 01/59] draft for simplified workflow run just syncs the active node id with the graph for now --- client/src/components/Form/FormDisplay.vue | 7 +- client/src/components/Form/FormInputs.vue | 15 +- .../Workflow/Editor/WorkflowGraph.vue | 3 +- .../Invocation/Graph/InvocationGraph.vue | 1 + .../components/Workflow/Run/WorkflowRun.vue | 2 +- .../Workflow/Run/WorkflowRunFormSimple.vue | 414 +++++++++--------- .../Workflow/Run/WorkflowRunInfo.vue | 74 ++++ 7 files changed, 311 insertions(+), 205 deletions(-) create mode 100644 client/src/components/Workflow/Run/WorkflowRunInfo.vue diff --git a/client/src/components/Form/FormDisplay.vue b/client/src/components/Form/FormDisplay.vue index e51dfe7e39b..5975714adfc 100644 --- a/client/src/components/Form/FormDisplay.vue +++ b/client/src/components/Form/FormDisplay.vue @@ -12,7 +12,8 @@ :collapsed-disable-icon="collapsedDisableIcon" :on-change="onChange" :on-change-form="onChangeForm" - :workflow-building-mode="workflowBuildingMode" /> + :workflow-building-mode="workflowBuildingMode" + :active-node-id="activeNodeId" /> + + diff --git a/client/src/components/Workflow/Editor/WorkflowGraph.vue b/client/src/components/Workflow/Editor/WorkflowGraph.vue index 91392ae14bb..8fa302b896e 100644 --- a/client/src/components/Workflow/Editor/WorkflowGraph.vue +++ b/client/src/components/Workflow/Editor/WorkflowGraph.vue @@ -37,6 +37,7 @@ const props = defineProps({ isInvocation: { type: Boolean, default: false }, showMinimap: { type: Boolean, default: true }, showZoomControls: { type: Boolean, default: true }, + fixedHeight: { type: Boolean, default: false }, }); const { stateStore, stepStore } = useWorkflowStores(); @@ -169,7 +170,7 @@ defineExpose({ id="canvas-container" ref="canvas" class="canvas-content" - :class="props.isInvocation ? 'fixed-window-height' : 'h-100'" + :class="props.fixedHeight ? 'fixed-window-height' : 'h-100'" @drop.prevent @dragover.prevent> diff --git a/client/src/components/Workflow/Run/WorkflowRun.vue b/client/src/components/Workflow/Run/WorkflowRun.vue index 47865217019..5f28caf39b5 100644 --- a/client/src/components/Workflow/Run/WorkflowRun.vue +++ b/client/src/components/Workflow/Run/WorkflowRun.vue @@ -208,7 +208,7 @@ defineExpose({ v-else-if="invocations.length > 0" :invocations="invocations" :workflow-name="workflowName" /> -
+
+import { BAlert, BDropdown, BDropdownForm, BFormCheckbox } from "bootstrap-vue"; +import { storeToRefs } from "pinia"; +import { computed, ref, set } from "vue"; + +import { allowCachedJobs } from "@/components/Tool/utilities"; +import { isWorkflowInput } from "@/components/Workflow/constants"; +import { useConfig } from "@/composables/config"; +import { provideScopedWorkflowStores } from "@/composables/workflowStores"; +import { useUserStore } from "@/stores/userStore"; +import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; +import { errorMessageAsString } from "@/utils/simple-error"; + +import { invokeWorkflow } from "./services"; + +import WorkflowAnnotation from "../WorkflowAnnotation.vue"; +import WorkflowNavigationTitle from "../WorkflowNavigationTitle.vue"; +import WorkflowRunInfo from "./WorkflowRunInfo.vue"; +import WorkflowStorageConfiguration from "./WorkflowStorageConfiguration.vue"; +import FormDisplay from "@/components/Form/FormDisplay.vue"; +import FlexPanel from "@/components/Panels/FlexPanel.vue"; + +interface Props { + model: Record; + targetHistory?: string; + useJobCache?: boolean; + canMutateCurrentHistory: boolean; + requestState?: Record; +} + +const props = withDefaults(defineProps(), { + targetHistory: "current", + useJobCache: false, + requestState: undefined, +}); + +const emit = defineEmits<{ + (e: "showAdvanced"): void; + (e: "submissionSuccess", invocations: any): void; + (e: "submissionError", error: string): void; +}>(); + +provideScopedWorkflowStores(props.model.workflowId); + +const { activeNodeId } = storeToRefs(useWorkflowStateStore(props.model.workflowId)); + +const { config, isConfigLoaded } = useConfig(true); +const { currentUser } = storeToRefs(useUserStore()); + +const formData = ref>({}); +const inputTypes = ref>({}); +const stepValidations = ref>({}); +const sendToNewHistory = ref(props.targetHistory === "new" || props.targetHistory === "prefer_new"); +const useCachedJobs = ref(props.useJobCache); +const splitObjectStore = ref(false); +const preferredObjectStoreId = ref(null); +const preferredIntermediateObjectStoreId = ref(null); +const waitingForRequest = ref(false); + +const formInputs = computed(() => { + const inputs = [] as any[]; + // Add workflow parameters. + Object.values(props.model.wpInputs).forEach((input) => { + const inputCopy = Object.assign({}, input) as any; + // do we want to keep the color if we're not showing steps? + inputCopy.color = undefined; + inputs.push(inputCopy); + inputTypes.value[inputCopy.name] = "replacement_parameter"; + }); + // Add actual input modules. + props.model.steps.forEach((step: any, i: number) => { + if (isWorkflowInput(step.step_type)) { + const stepName = new String(step.step_index) as any; + const stepLabel = step.step_label || new String(step.step_index + 1); + const stepType = step.step_type; + const help = step.annotation; + const longFormInput = step.inputs[0]; + const stepAsInput = Object.assign({}, longFormInput, { + name: stepName, + help: help, + label: stepLabel, + }); + if (props.requestState && props.requestState[stepLabel]) { + const value = props.requestState[stepLabel]; + stepAsInput.value = value; + } + // disable collection mapping... + stepAsInput.flavor = "module"; + inputs.push(stepAsInput); + inputTypes.value[stepName] = stepType; + } + }); + return inputs; +}); + +const hasValidationErrors = computed(() => { + return Boolean(Object.values(stepValidations.value).find((value) => value !== null && value !== undefined)); +}); + +const canRunOnHistory = computed(() => props.canMutateCurrentHistory || sendToNewHistory.value); + +function onValidation(validation: any) { + if (validation) { + set(stepValidations.value, validation[0], validation[1]); + } else { + stepValidations.value = {}; + } +} + +function reuseAllowed(user: any) { + return user && allowCachedJobs(user.preferences); +} + +function showRuntimeSettings(user: any) { + return props.targetHistory && (props.targetHistory.indexOf("prefer") >= 0 || (user && reuseAllowed(user))); +} + +function onChange(data: any) { + formData.value = data; +} + +function onStorageUpdate(objectStoreId: string, intermediate: boolean) { + if (intermediate) { + preferredIntermediateObjectStoreId.value = objectStoreId; + } else { + preferredObjectStoreId.value = objectStoreId; + } +} + +async function onExecute() { + waitingForRequest.value = true; + + const replacementParams: Record = {}; + const inputs: Record = {}; + for (const inputName in formData.value) { + const value = formData.value[inputName]; + const inputType = inputTypes.value[inputName]; + if (inputType == "replacement_parameter") { + replacementParams[inputName] = value; + } else if (inputType && isWorkflowInput(inputType)) { + inputs[inputName] = value; + } + } + const data: Record = { + replacement_dict: replacementParams, + inputs: inputs, + inputs_by: "step_index", + batch: true, + use_cached_job: useCachedJobs.value, + require_exact_tool_versions: false, + version: props.model.runData.version, + }; + if (sendToNewHistory.value) { + data.new_history_name = props.model.name; + } else { + data.history_id = props.model.historyId; + } + if (splitObjectStore.value) { + if (preferredObjectStoreId.value != null) { + data.preferred_outputs_object_store_id = preferredObjectStoreId.value; + } + if (preferredIntermediateObjectStoreId.value != null && splitObjectStore.value) { + data.preferred_intermediate_object_store_id = preferredIntermediateObjectStoreId.value; + } + } else { + if (preferredObjectStoreId.value != null) { + data.preferred_object_store_id = preferredObjectStoreId.value; + } + } + + try { + const invocations = await invokeWorkflow(props.model.workflowId, data); + emit("submissionSuccess", invocations); + } catch (error) { + emit("submissionError", errorMessageAsString(error)); + } finally { + waitingForRequest.value = false; + } +} + +
- - - Expand to full workflow form. + +
- - diff --git a/client/src/components/Workflow/Run/WorkflowRunInfo.vue b/client/src/components/Workflow/Run/WorkflowRunInfo.vue new file mode 100644 index 00000000000..01d8f784d9a --- /dev/null +++ b/client/src/components/Workflow/Run/WorkflowRunInfo.vue @@ -0,0 +1,74 @@ + + + From 8796f3ff6d9e5482d3ade0b2f58d5291005d25a1 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Mon, 9 Dec 2024 14:49:31 -0600 Subject: [PATCH 02/59] have a functioning side by side inputs w/graph run form The input values are synced with the graph --- client/src/components/Form/FormDisplay.vue | 8 +- client/src/components/Form/FormInputs.vue | 34 ++++- .../src/components/Workflow/Editor/Node.vue | 24 +++- .../Workflow/Editor/WorkflowGraph.vue | 2 + .../Workflow/Run/WorkflowRunFormSimple.vue | 40 ++++-- .../Workflow/Run/WorkflowRunGraph.vue | 130 ++++++++++++++++++ .../Workflow/Run/WorkflowRunInfo.vue | 74 ---------- 7 files changed, 216 insertions(+), 96 deletions(-) create mode 100644 client/src/components/Workflow/Run/WorkflowRunGraph.vue delete mode 100644 client/src/components/Workflow/Run/WorkflowRunInfo.vue diff --git a/client/src/components/Form/FormDisplay.vue b/client/src/components/Form/FormDisplay.vue index 5975714adfc..b0d44751284 100644 --- a/client/src/components/Form/FormDisplay.vue +++ b/client/src/components/Form/FormDisplay.vue @@ -13,7 +13,9 @@ :on-change="onChange" :on-change-form="onChangeForm" :workflow-building-mode="workflowBuildingMode" - :active-node-id="activeNodeId" /> + :active-node-id="activeNodeId" + :sync-with-graph="syncWithGraph" + @update:active-node-id="($event) => $emit('update:active-node-id', $event)" /> + + diff --git a/client/src/components/Workflow/Run/WorkflowRunInfo.vue b/client/src/components/Workflow/Run/WorkflowRunInfo.vue deleted file mode 100644 index 01d8f784d9a..00000000000 --- a/client/src/components/Workflow/Run/WorkflowRunInfo.vue +++ /dev/null @@ -1,74 +0,0 @@ - - - From 871939df5322ba877616eb2c705da762fff8020a Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Fri, 17 Jan 2025 11:57:38 -0600 Subject: [PATCH 03/59] typed messages for steps on graph, change `stepValidation` to array - We show the appropriate, typed step description for unpopulated or errored steps on the `WorkflowRunGraph` - Changed the `stepValidations` object to an array of `[index, msg]` since we can use the return of `validateInputs` as is, and hence, track only the latest received error message. --- .../Workflow/Run/WorkflowRunFormSimple.vue | 44 ++++-- .../Workflow/Run/WorkflowRunGraph.vue | 125 +++++++++++++++--- 2 files changed, 134 insertions(+), 35 deletions(-) diff --git a/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue b/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue index 1ac29af1124..25144482c2f 100644 --- a/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue +++ b/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue @@ -3,7 +3,7 @@ import { faSitemap } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { BAlert, BButton, BDropdown, BDropdownForm, BFormCheckbox } from "bootstrap-vue"; import { storeToRefs } from "pinia"; -import { computed, ref, set } from "vue"; +import { computed, ref, watch } from "vue"; import { allowCachedJobs } from "@/components/Tool/utilities"; import { isWorkflowInput } from "@/components/Workflow/constants"; @@ -50,7 +50,7 @@ const { showPanels } = usePanels(); const formData = ref>({}); const inputTypes = ref>({}); -const stepValidations = ref>({}); +const stepValidation = ref<[string, string] | null>(null); const sendToNewHistory = ref(props.targetHistory === "new" || props.targetHistory === "prefer_new"); const useCachedJobs = ref(props.useJobCache); const splitObjectStore = ref(false); @@ -62,6 +62,23 @@ const waitingForRequest = ref(false); // readme/help exists, it will be `false`. const showGraph = ref(!showPanels.value); +watch( + () => showGraph.value, + (show) => { + if (!show) { + activeNodeId.value = null; + } + } +); +const computedActiveNodeId = computed(() => { + if (showGraph.value) { + if (activeNodeId.value !== null && activeNodeId.value !== undefined) { + return activeNodeId.value; + } + } + return undefined; +}); + const formInputs = computed(() => { const inputs = [] as any[]; // Add workflow parameters. @@ -98,17 +115,15 @@ const formInputs = computed(() => { return inputs; }); -const hasValidationErrors = computed(() => { - return Boolean(Object.values(stepValidations.value).find((value) => value !== null && value !== undefined)); -}); +const hasValidationErrors = computed(() => stepValidation.value !== null); const canRunOnHistory = computed(() => props.canMutateCurrentHistory || sendToNewHistory.value); -function onValidation(validation: any) { - if (validation) { - set(stepValidations.value, validation[0], validation[1]); +function onValidation(validation: [string, string] | null) { + if (validation && validation.length == 2) { + stepValidation.value = [validation[0], validation[1]]; } else { - stepValidations.value = {}; + stepValidation.value = null; } } @@ -252,15 +267,17 @@ async function onExecute() {
-
- Parameters +
+ Parameters + @onValidation="onValidation" + @update:active-node-id="($event) => (activeNodeId = $event)" /> Expand to full workflow form. @@ -270,6 +287,7 @@ async function onExecute() { ; + inputs: Record; + stepValidation: any; // TODO: type as [string, string] | null; formInputs: any[]; } @@ -69,39 +73,117 @@ function syncStepsWithInputVals() { for (const s of Object.values(loadedWorkflow.value?.steps)) { const step = s as any; if (isWorkflowInput(step.type)) { - const dataInput = props.inputs[step.id.toString()]; - const inputVals = dataInput?.values; - + let dataInput = props.inputs[step.id.toString()]; const formInput = props.formInputs.find((input) => parseInt(input.name) === step.id); - const options = formInput?.options; + const optional = formInput?.optional as boolean; - if (inputVals?.length === 1 && inputVals[0]) { - const { id, src } = inputVals[0]; - const item = options[src].find((option: any) => option.id === id); - set(step, "nodeText", `${item.hid}: ${item.name}`); - set(step, "headerClass", getHeaderClass("ok")); - set(step, "headerIcon", faCheckCircle); - } else if (inputVals?.length) { - set(step, "nodeText", `${inputVals.length} inputs provided`); - set(step, "headerClass", getHeaderClass("ok")); - set(step, "headerIcon", faCheckCircle); + if (formInput.model_class === "BooleanToolParameter") { + setStepDescription(step, dataInput as boolean, true); + } else if ( + ["TextToolParameter", "IntegerToolParameter", "FloatToolParameter"].includes(formInput.model_class) + ) { + if (!dataInput || dataInput.toString().trim() === "") { + let infoText: string; + switch (formInput.model_class) { + case "TextToolParameter": + infoText = `Provide text input${optional ? " (optional)" : ""}`; + break; + case "IntegerToolParameter": + infoText = `Provide an integer${optional ? " (optional)" : ""}`; + break; + default: + infoText = `Provide a float${optional ? " (optional)" : ""}`; + break; + } + setStepDescription(step, infoText, false, optional); + } else { + setStepDescription(step, `${dataInput}`, true); + } + } else if (formInput.model_class === "ColorToolParameter") { + if (!dataInput) { + setStepDescription(step, "Provide a color", false); + } else { + setStepDescription(step, dataInput as string, true); + } + } else if (formInput.model_class === "DirectoryUriToolParameter") { + if (!dataInput) { + setStepDescription(step, `Provide a directory${optional ? " (optional)" : ""}`, false, optional); + } else { + setStepDescription(step, `Directory: ${dataInput}`, true); + } + } else if ( + formInput.model_class === "DataToolParameter" || + formInput.model_class === "DataCollectionToolParameter" + ) { + dataInput = dataInput as DataToolParameterInput | DataCollectionToolParameterInput; + const inputVals = dataInput?.values; + const options = formInput?.options; + + if (inputVals?.length === 1 && inputVals[0]) { + const { id, src } = inputVals[0]; + const item = options[src].find((option: any) => option.id === id); + setStepDescription(step, `${item.hid}: ${item.name}`, true); + } else if (inputVals?.length) { + setStepDescription(step, `${inputVals.length} inputs provided`, true); + } else if (formInput.model_class === "DataToolParameter") { + setStepDescription(step, `Provide a dataset${optional ? " (optional)" : ""}`, false, optional); + } else { + setStepDescription(step, `Provide a collection${optional ? " (optional)" : ""}`, false, optional); + } } else { - set(step, "nodeText", "Populate this input"); - - // color variant for `running` state works best - set(step, "headerClass", getHeaderClass("running")); - set(step, "headerIcon", faExclamationCircle); + set(step, "nodeText", "This is an input"); } } } } +/** Annotate the step for the workflow graph with the current input value or prompt + * @param step The step to annotate + * @param text The text to display + * @param populated Whether the input is populated, undefined for optional inputs + * @param optional Whether the input is optional + */ +function setStepDescription(step: any, text: string | boolean, populated: boolean, optional?: boolean) { + // color variant for `paused` state works best for unpopulated inputs, + // "" for optional inputs and `ok` for populated inputs + const headerClass = optional ? "" : populated ? "ok" : "paused"; + const headerIcon = populated ? faCheckCircle : faExclamationCircle; + + set(step, "nodeText", text); + set(step, "headerClass", getHeaderClass(headerClass)); + set(step, "headerIcon", headerIcon); +} + +function setStepDescriptionForValidation(): boolean { + if (props.stepValidation && props.stepValidation.length == 2) { + const [stepId, message] = props.stepValidation; + const step = loadedWorkflow.value.steps[stepId]; + if (step) { + const text = message.length < 20 ? message : "Fix errors for this step"; + setStepDescription(step, text, false); + return true; + } + } + return false; +} + watch( () => props.inputs, () => { syncStepsWithInputVals(); } ); + +watch( + () => props.stepValidation, + () => { + if (!loadedWorkflow.value?.steps) { + return; + } + setStepDescriptionForValidation(); + }, + { immediate: true } +);