Skip to content

Commit

Permalink
Add an invocation graph view to the invocation summary
Browse files Browse the repository at this point in the history
This graph view utilizes the same workflow editor canvas, and instead of showing step details on each step node, it shows the job states for the steps. Clicking on a step expands the details for the invocation step.

The `useInvocationGraph` composable loads the graph based on the original workflow id and invocation object; and the step info is loaded via the `step_jobs_summary` api route.
  • Loading branch information
ahmedhamidawan committed Apr 11, 2024
1 parent 180e464 commit 28e9fd0
Show file tree
Hide file tree
Showing 15 changed files with 949 additions and 96 deletions.
16 changes: 12 additions & 4 deletions client/src/components/History/Content/ExpandedItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default {
props: {
scopeKey: { type: String, required: true },
getItemKey: { type: Function, required: true },
explicitKey: { type: String, required: false },
},
data() {
return {
Expand Down Expand Up @@ -37,14 +38,21 @@ export default {
}
},
items(newSet) {
if (this.explicitKey) {
return;
}
saveSet(this.key, newSet);
},
},
created() {
this.key = "expanded-history-items";
const cachedSet = loadSet(this.key);
if (cachedSet) {
this.items = cachedSet;
if (this.explicitKey) {
this.key = this.explicitKey;
} else {
this.key = "expanded-history-items";
const cachedSet = loadSet(this.key);
if (cachedSet) {
this.items = cachedSet;
}
}
},
render() {
Expand Down
59 changes: 54 additions & 5 deletions client/src/components/Workflow/Editor/Node.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
:disabled="readonly"
@move="onMoveTo"
@pan-by="onPanBy">
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
<div
class="node-header unselectable clearfix card-header py-1 px-2"
class="unselectable clearfix card-header py-1 px-2"
:class="headerClass"
@click="makeActive"
@keyup.enter="makeActive">
<b-button-group class="float-right">
Expand Down Expand Up @@ -74,6 +76,12 @@
>{{ step.id + 1 }}:
</span>
<span class="node-title">{{ title }}</span>
<span class="float-right">
<FontAwesomeIcon
v-if="isInvocation && invocationStep.headerIcon"
:icon="invocationStep.headerIcon"
:spin="invocationStep.headerIconSpin" />
</span>
</div>
<b-alert
v-if="!!errors"
Expand All @@ -83,11 +91,19 @@
@click="makeActive">
{{ errors }}
</b-alert>
<div v-else class="node-body card-body p-0 mx-2" @click="makeActive" @keyup.enter="makeActive">
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
<div
v-else
class="node-body position-relative card-body p-0 mx-2"
:class="{ 'cursor-pointer': isInvocation }"
@click="makeActive"
@keyup.enter="makeActive">
<NodeInput
v-for="(input, index) in inputs"
:key="`in-${index}-${input.name}`"
:class="isInvocation && 'position-absolute'"
:input="input"
:blank="isInvocation"
:step-id="id"
:datatypes-mapper="datatypesMapper"
:step-position="step.position ?? { top: 0, left: 0 }"
Expand All @@ -97,13 +113,16 @@
:parent-node="elHtml"
:readonly="readonly"
@onChange="onChange" />
<div v-if="showRule" class="rule" />
<div v-if="!isInvocation && showRule" class="rule" />
<NodeInvocationText v-if="isInvocation" :invocation-step="invocationStep" />
<NodeOutput
v-for="(output, index) in outputs"
:key="`out-${index}-${output.name}`"
:class="isInvocation && 'invocation-node-output'"
:output="output"
:workflow-outputs="workflowOutputs"
:post-job-actions="postJobActions"
:blank="isInvocation"
:step-id="id"
:step-type="step.type"
:step-position="step.position ?? { top: 0, left: 0 }"
Expand Down Expand Up @@ -133,6 +152,7 @@ import { getGalaxyInstance } from "@/app";
import { DatatypesMapperModel } from "@/components/Datatypes/model";
import { useNodePosition } from "@/components/Workflow/Editor/composables/useNodePosition";
import WorkflowIcons from "@/components/Workflow/icons";
import type { GraphStep } from "@/composables/useInvocationGraph";
import { useWorkflowStores } from "@/composables/workflowStores";
import type { TerminalPosition, XYPosition } from "@/stores/workflowEditorStateStore";
import type { Step } from "@/stores/workflowStepStore";
Expand All @@ -142,6 +162,7 @@ import type { OutputTerminals } from "./modules/terminals";
import LoadingSpan from "@/components/LoadingSpan.vue";
import DraggableWrapper from "@/components/Workflow/Editor/DraggablePan.vue";
import NodeInput from "@/components/Workflow/Editor/NodeInput.vue";
import NodeInvocationText from "@/components/Workflow/Editor/NodeInvocationText.vue";
import NodeOutput from "@/components/Workflow/Editor/NodeOutput.vue";
import Recommendations from "@/components/Workflow/Editor/Recommendations.vue";
Expand All @@ -153,7 +174,7 @@ const props = defineProps({
id: { type: Number, required: true },
contentId: { type: String as PropType<string | null>, default: null },
name: { type: String as PropType<string | null>, default: null },
step: { type: Object as PropType<Step>, required: true },
step: { type: Object as PropType<Step | GraphStep>, required: true },
datatypesMapper: { type: DatatypesMapperModel, required: true },
activeNodeId: {
type: null as unknown as PropType<number | null>,
Expand All @@ -164,6 +185,7 @@ const props = defineProps({
scroll: { type: Object as PropType<UseScrollReturn>, required: true },
scale: { type: Number, default: 1 },
highlight: { type: Boolean, default: false },
isInvocation: { type: Boolean, default: false },
readonly: { type: Boolean, default: false },
});
Expand Down Expand Up @@ -220,6 +242,23 @@ const style = computed(() => {
return { top: props.step.position!.top + "px", left: props.step.position!.left + "px" };
});
const errors = computed(() => props.step.errors || stateStore.getStepLoadingState(props.id)?.error);
const headerClass = computed(() => {
let cls;
if (props.isInvocation) {
cls = "cursor-pointer";
if (invocationStep.value.headerClass) {
cls += ` ${invocationStep.value.headerClass}`;
} else {
cls += " node-header";
}
} else {
cls = "node-header";
if (!props.readonly && !props.isInvocation) {
cls += " cursor-move";
}
}
return cls;
});
const inputs = computed(() => {
const connections = connectionStore.getConnectionsForStep(props.id);
const extraStepInputs = stepStore.getStepExtraInputs(props.id);
Expand Down Expand Up @@ -248,6 +287,7 @@ const invalidOutputs = computed(() => {
return { name, optional: false, datatypes: [], valid: false };
});
});
const invocationStep = computed(() => props.step as GraphStep);
const outputs = computed(() => {
return [...props.step.outputs, ...invalidOutputs.value];
});
Expand Down Expand Up @@ -318,12 +358,21 @@ function makeActive() {
}
.node-header {
cursor: move;
background: $brand-primary;
color: $white;
&.cursor-move {
cursor: move;
}
}
.node-body {
.invocation-node-output {
position: absolute;
right: 0;
top: 0;
width: 100%;
height: 100%;
}
.rule {
height: 0;
border: none;
Expand Down
38 changes: 22 additions & 16 deletions client/src/components/Workflow/Editor/NodeInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
blank: {
type: Boolean,
default: false,
},
});
onBeforeUnmount(() => {
Expand Down Expand Up @@ -144,7 +148,7 @@ const label = computed(() => props.input.label || props.input.name);
const hasConnections = computed(() => connections.value.length > 0);
const rowClass = computed(() => {
const classes = ["form-row", "dataRow", "input-data-row"];
if (props.input?.valid === false) {
if (!props.blank && props.input?.valid === false) {
classes.push("form-row-error");
}
return classes;
Expand Down Expand Up @@ -234,21 +238,23 @@ watch(
</b-tooltip>
<FontAwesomeIcon class="terminal-icon" icon="fa-chevron-circle-right" />
</div>
<button
v-if="hasConnections && !readonly"
v-b-tooltip.hover
:title="reason"
class="delete-terminal-button"
@click="onRemove">
<FontAwesomeIcon class="delete-button-icon" icon="fa-minus-square" />
</button>
{{ label }}
<span
v-if="!input.optional && !hasTerminals"
v-b-tooltip.hover
class="input-required"
title="Input is required">
*
<span v-if="!blank">
<button
v-if="hasConnections && !readonly"
v-b-tooltip.hover
:title="reason"
class="delete-terminal-button"
@click="onRemove">
<FontAwesomeIcon class="delete-button-icon" icon="fa-minus-square" />
</button>
{{ label }}
<span
v-if="!input.optional && !hasTerminals"
v-b-tooltip.hover
class="input-required"
title="Input is required">
*
</span>
</span>
</div>
</template>
Expand Down
39 changes: 39 additions & 0 deletions client/src/components/Workflow/Editor/NodeInvocationText.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { isWorkflowInput } from "@/components/Workflow/constants";
import { type GraphStep, iconClasses } from "@/composables/useInvocationGraph";
const props = defineProps<{
invocationStep: GraphStep;
}>();
const statePlaceholders: Record<string, string> = {
ok: "successful",
error: "failed",
};
</script>
<template>
<div class="p-1 unselectable">
<div v-if="props.invocationStep.jobs">
<div v-for="(value, key) in props.invocationStep.jobs" :key="key">
<span v-if="value !== undefined" class="d-flex align-items-center">
<FontAwesomeIcon
v-if="iconClasses[key]"
:icon="iconClasses[key]?.icon"
:class="iconClasses[key]?.class"
:spin="iconClasses[key]?.spin"
size="sm"
class="mr-1" />
{{ value }} job{{ value > 1 ? "s" : "" }} {{ statePlaceholders[key] || key }}.
</span>
</div>
</div>
<div v-else-if="isWorkflowInput(props.invocationStep.type)">
<!-- TODO: Maybe put a ContentItem here? -->
This is an input
</div>
<div v-else-if="props.invocationStep.type === 'subworkflow'">This is a subworkflow.</div>
<div v-else>This step has no jobs as of yet.</div>
</div>
</template>
3 changes: 2 additions & 1 deletion client/src/components/Workflow/Editor/NodeOutput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const props = defineProps<{
datatypesMapper: DatatypesMapperModel;
parentNode: HTMLElement | null;
readonly: boolean;
blank: boolean;
}>();
const emit = defineEmits(["pan-by", "stopDragging", "onDragConnector"]);
Expand Down Expand Up @@ -321,7 +322,7 @@ const removeTagsAction = computed(() => {

<template>
<div class="node-output" :class="rowClass" :data-output-name="output.name">
<div class="d-flex flex-column w-100">
<div v-if="!props.blank" class="d-flex flex-column w-100">
<div class="node-output-buttons">
<button
v-if="showCalloutActiveOutput"
Expand Down
16 changes: 14 additions & 2 deletions client/src/components/Workflow/Editor/WorkflowGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
@onZoom="onZoom"
@update:pan="panBy" />
<ToolBar v-if="!readonly" />
<div id="canvas-container" ref="canvas" class="canvas-content" @drop.prevent @dragover.prevent>
<div
id="canvas-container"
ref="canvas"
class="canvas-content"
:class="props.isInvocation ? 'fixed-window-height' : 'h-100'"
@drop.prevent
@dragover.prevent>
<AdaptiveGrid
:viewport-bounds="elementBounding"
:viewport-bounding-box="viewportBoundingBox"
Expand All @@ -31,6 +37,7 @@
:scroll="scroll"
:scale="scale"
:readonly="readonly"
:is-invocation="props.isInvocation"
@pan-by="panBy"
@stopDragging="onStopDragging"
@onDragConnector="onDragConnector"
Expand Down Expand Up @@ -91,6 +98,7 @@ const props = defineProps({
scrollToId: { type: Number as PropType<number | null>, default: null },
readonly: { type: Boolean, default: false },
initialPosition: { type: Object as PropType<{ x: number; y: number }>, default: () => ({ x: 50, y: 20 }) },
isInvocation: { type: Boolean, default: false },
showMinimap: { type: Boolean, default: true },
showZoomControls: { type: Boolean, default: true },
});
Expand Down Expand Up @@ -198,11 +206,15 @@ const { comments } = storeToRefs(commentStore);
.canvas-content {
width: 100%;
height: 100%;
position: relative;
left: 0px;
top: 0px;
overflow: hidden;
/* TODO: w/out this, canvas height = 0 when width goes beyond a point (invocation graph) */
&.fixed-window-height {
height: 50vh;
}
}
.node-area {
Expand Down
Loading

0 comments on commit 28e9fd0

Please sign in to comment.