Loading client/src/components/Workflow/Run/WorkflowRunGraph.vue +2 −2 Original line number Diff line number Diff line Loading @@ -30,7 +30,7 @@ const { activeNodeId } = storeToRefs(useWorkflowStateStore(props.workflowId)); const datatypesMapperStore = useDatatypesMapperStore(); const { datatypesMapper, loading: datatypesMapperLoading } = storeToRefs(datatypesMapperStore); const { steps, loading, loadWorkflowRunGraph } = useWorkflowRunGraph( const { steps, loading, loadWorkflowOntoGraph } = useWorkflowRunGraph( props.workflowId, props.version, toRef(props, "inputs"), Loading @@ -39,7 +39,7 @@ const { steps, loading, loadWorkflowRunGraph } = useWorkflowRunGraph( ); try { loadWorkflowRunGraph(); loadWorkflowOntoGraph(); } catch (error) { errorMessage.value = errorMessageAsString(error); } Loading client/src/composables/useWorkflowRunGraph.ts +96 −72 Original line number Diff line number Diff line import { faCheckCircle, faExclamationCircle, type IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { computed, type Ref, ref, set } from "vue"; import { faCheckCircle, faExclamationCircle, faSpinner, type IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { computed, type Ref, ref } from "vue"; import { isWorkflowInput } from "@/components/Workflow/constants"; import { fromSimple } from "@/components/Workflow/Editor/modules/model"; Loading Loading @@ -30,12 +30,15 @@ interface DataToolParameterInput extends BaseDataToolParameterInput {} interface DataCollectionToolParameterInput extends BaseDataToolParameterInput {} export type DataInput = DataToolParameterInput | DataCollectionToolParameterInput | boolean | string | null; interface WorkflowRunStep extends Readonly<Step> { interface WorkflowRunStepInfo { headerClass?: Record<string, boolean>; headerIcon?: IconDefinition; headerIconSpin?: boolean; nodeText?: string | boolean; } interface WorkflowRunStep extends Readonly<Step>, WorkflowRunStepInfo {} /** Composable that creates a readonly workflow run graph and loads it onto a workflow editor canvas for display. * This graph updates as the user changes the inputs of the workflow. * @param workflowId - The id of the workflow Loading @@ -56,7 +59,7 @@ export function useWorkflowRunGraph( const loading = ref(true); async function loadWorkflowRunGraph() { async function loadWorkflowOntoGraph() { loading.value = true; try { Loading @@ -80,25 +83,45 @@ export function useWorkflowRunGraph( } } /** The steps of the original workflow */ const workflowSteps = computed<Record<string, Readonly<Step>>>(() => loadedWorkflow.value?.steps); const steps = computed<{ [index: string]: WorkflowRunStep }>(() => { const steps = computed<Record<string, WorkflowRunStep>>(() => { if (!workflowSteps.value || !formInputs.value || !inputs.value) { return {}; } return Object.keys(workflowSteps.value).reduce((acc: { [index: string]: WorkflowRunStep }, k: string) => { const step = { ...workflowSteps.value[k] } as WorkflowRunStep; const key = parseInt(k); const result: Record<string, WorkflowRunStep> = {}; for (const stepId in workflowSteps.value) { const step = workflowSteps.value[stepId]; if (step) { if (isWorkflowInput(step.type) && !checkAndSetStepDescriptionForValidation(step)) { let dataInput = inputs.value[step.id.toString()]; let stepInfo: WorkflowRunStepInfo | null = null; const validation = getWorkflowRunStepValidation(step); if (validation) { stepInfo = validation; } else if (isWorkflowInput(step.type)) { const dataInput = inputs.value[step.id.toString()]; const formInput = formInputs.value.find((input) => parseInt(input.name) === step.id); stepInfo = getWorkflowRunStepInfo(formInput, dataInput); } if (stepInfo) { result[stepId] = { ...step, ...stepInfo }; } } } return result; }); /** Return step desciptions for the workflow graph given the current input field and user value * @param formInput The form input field * @param dataInput The user input value */ function getWorkflowRunStepInfo(formInput: any, dataInput?: DataInput): WorkflowRunStepInfo { const optional = formInput?.optional as boolean; const modelClass = formInput?.model_class as keyof typeof STEP_DESCRIPTIONS; if (modelClass === "BooleanToolParameter") { setStepDescription(step, dataInput as boolean, true); return getStepDescription(dataInput as boolean, true); } else if (modelClass === "DataToolParameter" || modelClass === "DataCollectionToolParameter") { dataInput = dataInput as DataToolParameterInput | DataCollectionToolParameterInput; const inputVals = dataInput?.values; Loading @@ -109,16 +132,18 @@ export function useWorkflowRunGraph( const item = options[src].find((option: any) => option.id === id); if (item && item.hid && item.name) { setStepDescription(step, `${item.hid}: <b>${item.name}</b>`, true); return getStepDescription(`${item.hid}: <b>${item.name}</b>`, true); } else { return getStepDescription("Input value processing", true, optional, true); } } else if (inputVals?.length) { setStepDescription(step, `${inputVals.length} inputs provided`, true); return getStepDescription(`${inputVals.length} inputs provided`, true); } else { setStepDescription(step, STEP_DESCRIPTIONS[modelClass], false, optional); return getStepDescription(STEP_DESCRIPTIONS[modelClass], false, optional); } } else if (Object.keys(STEP_DESCRIPTIONS).includes(modelClass)) { if (!dataInput || dataInput.toString().trim() === "") { setStepDescription(step, STEP_DESCRIPTIONS[modelClass], false, optional); return getStepDescription(STEP_DESCRIPTIONS[modelClass], false, optional); } else { let text: string; switch (modelClass) { Loading @@ -131,58 +156,57 @@ export function useWorkflowRunGraph( default: text = `<b>${dataInput}</b>`; } setStepDescription(step, text, true); return getStepDescription(text, true); } } else { set(step, "nodeText", "This is an input"); } return getStepDescription("This is an input", true); } acc[key] = step; } return acc; }, {}); }); /** Annotate the step for the workflow graph with the current input value or prompt * @param step The step to annotate /** Return step desciptions for the workflow graph given the current input value or prompt * @param text The text to display * @param populated Whether the input is populated, undefined for optional inputs * @param optional Whether the input is optional * @param spin Whether the icon should spin */ function setStepDescription(step: WorkflowRunStep, text: string | boolean, populated: boolean, optional?: boolean) { function getStepDescription( text: string | boolean, populated: boolean, optional?: boolean, spin?: boolean ): WorkflowRunStepInfo { // 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; const headerClass = optional ? "" : !spin && populated ? "ok" : "paused"; const headerIcon = spin ? faSpinner : populated ? faCheckCircle : faExclamationCircle; text = typeof text === "boolean" ? text : !optional ? text : `${text} (optional)`; set(step, "nodeText", text); set(step, "headerClass", getHeaderClass(headerClass)); set(step, "headerIcon", headerIcon); return { nodeText: text, headerClass: getHeaderClass(headerClass), headerIcon, headerIconSpin: spin, }; } function checkAndSetStepDescriptionForValidation(step: WorkflowRunStep): boolean { function getWorkflowRunStepValidation(step: Step): WorkflowRunStepInfo | null { if (stepValidation.value && stepValidation.value.length == 2) { const [stepId, message] = stepValidation.value; if (stepId === step.id.toString()) { const text = message.length < 20 ? message : "Fix error(s) for this step"; setStepDescription(step, text, false); return true; return getStepDescription(text, false); } } return false; return null; } return { /** The steps of the workflow run graph */ steps, /** Fetches the original workflow structure (once) and syncs the step * descriptions given the current user inputs. */ loadWorkflowRunGraph, /** Fetches the original workflow structure and loads it onto the graph */ loadWorkflowOntoGraph, loading, }; } Loading
client/src/components/Workflow/Run/WorkflowRunGraph.vue +2 −2 Original line number Diff line number Diff line Loading @@ -30,7 +30,7 @@ const { activeNodeId } = storeToRefs(useWorkflowStateStore(props.workflowId)); const datatypesMapperStore = useDatatypesMapperStore(); const { datatypesMapper, loading: datatypesMapperLoading } = storeToRefs(datatypesMapperStore); const { steps, loading, loadWorkflowRunGraph } = useWorkflowRunGraph( const { steps, loading, loadWorkflowOntoGraph } = useWorkflowRunGraph( props.workflowId, props.version, toRef(props, "inputs"), Loading @@ -39,7 +39,7 @@ const { steps, loading, loadWorkflowRunGraph } = useWorkflowRunGraph( ); try { loadWorkflowRunGraph(); loadWorkflowOntoGraph(); } catch (error) { errorMessage.value = errorMessageAsString(error); } Loading
client/src/composables/useWorkflowRunGraph.ts +96 −72 Original line number Diff line number Diff line import { faCheckCircle, faExclamationCircle, type IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { computed, type Ref, ref, set } from "vue"; import { faCheckCircle, faExclamationCircle, faSpinner, type IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { computed, type Ref, ref } from "vue"; import { isWorkflowInput } from "@/components/Workflow/constants"; import { fromSimple } from "@/components/Workflow/Editor/modules/model"; Loading Loading @@ -30,12 +30,15 @@ interface DataToolParameterInput extends BaseDataToolParameterInput {} interface DataCollectionToolParameterInput extends BaseDataToolParameterInput {} export type DataInput = DataToolParameterInput | DataCollectionToolParameterInput | boolean | string | null; interface WorkflowRunStep extends Readonly<Step> { interface WorkflowRunStepInfo { headerClass?: Record<string, boolean>; headerIcon?: IconDefinition; headerIconSpin?: boolean; nodeText?: string | boolean; } interface WorkflowRunStep extends Readonly<Step>, WorkflowRunStepInfo {} /** Composable that creates a readonly workflow run graph and loads it onto a workflow editor canvas for display. * This graph updates as the user changes the inputs of the workflow. * @param workflowId - The id of the workflow Loading @@ -56,7 +59,7 @@ export function useWorkflowRunGraph( const loading = ref(true); async function loadWorkflowRunGraph() { async function loadWorkflowOntoGraph() { loading.value = true; try { Loading @@ -80,25 +83,45 @@ export function useWorkflowRunGraph( } } /** The steps of the original workflow */ const workflowSteps = computed<Record<string, Readonly<Step>>>(() => loadedWorkflow.value?.steps); const steps = computed<{ [index: string]: WorkflowRunStep }>(() => { const steps = computed<Record<string, WorkflowRunStep>>(() => { if (!workflowSteps.value || !formInputs.value || !inputs.value) { return {}; } return Object.keys(workflowSteps.value).reduce((acc: { [index: string]: WorkflowRunStep }, k: string) => { const step = { ...workflowSteps.value[k] } as WorkflowRunStep; const key = parseInt(k); const result: Record<string, WorkflowRunStep> = {}; for (const stepId in workflowSteps.value) { const step = workflowSteps.value[stepId]; if (step) { if (isWorkflowInput(step.type) && !checkAndSetStepDescriptionForValidation(step)) { let dataInput = inputs.value[step.id.toString()]; let stepInfo: WorkflowRunStepInfo | null = null; const validation = getWorkflowRunStepValidation(step); if (validation) { stepInfo = validation; } else if (isWorkflowInput(step.type)) { const dataInput = inputs.value[step.id.toString()]; const formInput = formInputs.value.find((input) => parseInt(input.name) === step.id); stepInfo = getWorkflowRunStepInfo(formInput, dataInput); } if (stepInfo) { result[stepId] = { ...step, ...stepInfo }; } } } return result; }); /** Return step desciptions for the workflow graph given the current input field and user value * @param formInput The form input field * @param dataInput The user input value */ function getWorkflowRunStepInfo(formInput: any, dataInput?: DataInput): WorkflowRunStepInfo { const optional = formInput?.optional as boolean; const modelClass = formInput?.model_class as keyof typeof STEP_DESCRIPTIONS; if (modelClass === "BooleanToolParameter") { setStepDescription(step, dataInput as boolean, true); return getStepDescription(dataInput as boolean, true); } else if (modelClass === "DataToolParameter" || modelClass === "DataCollectionToolParameter") { dataInput = dataInput as DataToolParameterInput | DataCollectionToolParameterInput; const inputVals = dataInput?.values; Loading @@ -109,16 +132,18 @@ export function useWorkflowRunGraph( const item = options[src].find((option: any) => option.id === id); if (item && item.hid && item.name) { setStepDescription(step, `${item.hid}: <b>${item.name}</b>`, true); return getStepDescription(`${item.hid}: <b>${item.name}</b>`, true); } else { return getStepDescription("Input value processing", true, optional, true); } } else if (inputVals?.length) { setStepDescription(step, `${inputVals.length} inputs provided`, true); return getStepDescription(`${inputVals.length} inputs provided`, true); } else { setStepDescription(step, STEP_DESCRIPTIONS[modelClass], false, optional); return getStepDescription(STEP_DESCRIPTIONS[modelClass], false, optional); } } else if (Object.keys(STEP_DESCRIPTIONS).includes(modelClass)) { if (!dataInput || dataInput.toString().trim() === "") { setStepDescription(step, STEP_DESCRIPTIONS[modelClass], false, optional); return getStepDescription(STEP_DESCRIPTIONS[modelClass], false, optional); } else { let text: string; switch (modelClass) { Loading @@ -131,58 +156,57 @@ export function useWorkflowRunGraph( default: text = `<b>${dataInput}</b>`; } setStepDescription(step, text, true); return getStepDescription(text, true); } } else { set(step, "nodeText", "This is an input"); } return getStepDescription("This is an input", true); } acc[key] = step; } return acc; }, {}); }); /** Annotate the step for the workflow graph with the current input value or prompt * @param step The step to annotate /** Return step desciptions for the workflow graph given the current input value or prompt * @param text The text to display * @param populated Whether the input is populated, undefined for optional inputs * @param optional Whether the input is optional * @param spin Whether the icon should spin */ function setStepDescription(step: WorkflowRunStep, text: string | boolean, populated: boolean, optional?: boolean) { function getStepDescription( text: string | boolean, populated: boolean, optional?: boolean, spin?: boolean ): WorkflowRunStepInfo { // 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; const headerClass = optional ? "" : !spin && populated ? "ok" : "paused"; const headerIcon = spin ? faSpinner : populated ? faCheckCircle : faExclamationCircle; text = typeof text === "boolean" ? text : !optional ? text : `${text} (optional)`; set(step, "nodeText", text); set(step, "headerClass", getHeaderClass(headerClass)); set(step, "headerIcon", headerIcon); return { nodeText: text, headerClass: getHeaderClass(headerClass), headerIcon, headerIconSpin: spin, }; } function checkAndSetStepDescriptionForValidation(step: WorkflowRunStep): boolean { function getWorkflowRunStepValidation(step: Step): WorkflowRunStepInfo | null { if (stepValidation.value && stepValidation.value.length == 2) { const [stepId, message] = stepValidation.value; if (stepId === step.id.toString()) { const text = message.length < 20 ? message : "Fix error(s) for this step"; setStepDescription(step, text, false); return true; return getStepDescription(text, false); } } return false; return null; } return { /** The steps of the workflow run graph */ steps, /** Fetches the original workflow structure (once) and syncs the step * descriptions given the current user inputs. */ loadWorkflowRunGraph, /** Fetches the original workflow structure and loads it onto the graph */ loadWorkflowOntoGraph, loading, }; }