Skip to content

Commit

Permalink
Merge pull request #17413 from ahmedhamidawan/graph_view_invocations
Browse files Browse the repository at this point in the history
Visualizing workflow runs with an invocation graph view
  • Loading branch information
dannon authored Apr 30, 2024
2 parents fd4165f + 2e53cb7 commit 516f541
Show file tree
Hide file tree
Showing 35 changed files with 1,802 additions and 397 deletions.
10 changes: 10 additions & 0 deletions client/src/api/invocations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ export type WorkflowInvocationCollectionView = components["schemas"]["WorkflowIn
export type InvocationJobsSummary = components["schemas"]["InvocationJobsResponse"];
export type InvocationStep = components["schemas"]["InvocationStep"];

export type StepJobSummary =
| components["schemas"]["InvocationStepJobsResponseStepModel"]
| components["schemas"]["InvocationStepJobsResponseJobModel"]
| components["schemas"]["InvocationStepJobsResponseCollectionJobsModel"];

export const invocationsFetcher = fetcher.path("/api/invocations").method("get").create();

export const stepJobsSummaryFetcher = fetcher
.path("/api/invocations/{invocation_id}/step_jobs_summary")
.method("get")
.create();

export type WorkflowInvocation = WorkflowInvocationElementView | WorkflowInvocationCollectionView;

export interface WorkflowInvocationJobsSummary {
Expand Down
9 changes: 7 additions & 2 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7963,6 +7963,11 @@ export interface components {
* @example 0123456789ABCDEF
*/
id: string;
/**
* Implicit Collection Jobs ID
* @description The implicit collection job ID associated with the workflow invocation step.
*/
implicit_collection_jobs_id?: string | null;
/** Job Id */
job_id: string | null;
/**
Expand Down Expand Up @@ -8049,7 +8054,7 @@ export interface components {
InvocationStepJobsResponseCollectionJobsModel: {
/**
* ID
* @description The encoded ID of the workflow invocation.
* @description The encoded ID of the collection job.
* @example 0123456789ABCDEF
*/
id: string;
Expand All @@ -8076,7 +8081,7 @@ export interface components {
InvocationStepJobsResponseJobModel: {
/**
* ID
* @description The encoded ID of the workflow invocation.
* @description The encoded ID of the job.
* @example 0123456789ABCDEF
*/
id: string;
Expand Down
4 changes: 3 additions & 1 deletion client/src/api/workflows.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { fetcher } from "@/api/schema";
import { components, fetcher } from "@/api/schema";

export type StoredWorkflowDetailed = components["schemas"]["StoredWorkflowDetailed"];

export const workflowsFetcher = fetcher.path("/api/workflows").method("get").create();

Expand Down
26 changes: 23 additions & 3 deletions client/src/components/Common/Heading.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faAngleDoubleDown, faAngleDoubleUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed } from "vue";
library.add(faAngleDoubleDown, faAngleDoubleUp);
interface Props {
h1?: boolean;
h2?: boolean;
Expand All @@ -14,17 +18,20 @@ interface Props {
inline?: boolean;
size?: "xl" | "lg" | "md" | "sm" | "text";
icon?: string | [string, string];
truncate?: boolean;
collapse?: "open" | "closed" | "none";
}
const props = withDefaults(defineProps<Props>(), {
collapse: "none",
icon: "",
size: "lg",
});
defineEmits(["click"]);
const sizeClass = computed(() => {
return `h-${props.size ?? "lg"}`;
return `h-${props.size}`;
});
const collapsible = computed(() => {
Expand Down Expand Up @@ -54,7 +61,12 @@ const element = computed(() => {
<div v-else class="stripe"></div>
<component
:is="element"
:class="[sizeClass, props.bold ? 'font-weight-bold' : '', collapsible ? 'collapsible' : '']"
:class="[
sizeClass,
props.bold ? 'font-weight-bold' : '',
collapsible ? 'collapsible' : '',
props.truncate ? 'truncate' : '',
]"
@click="$emit('click')">
<slot />
</component>
Expand Down Expand Up @@ -89,14 +101,22 @@ const element = computed(() => {
// prettier-ignore
h1, h2, h3, h4, h5, h6 {
display: flex;
&:not(.truncate) {
display: flex;
}
align-items: center;
gap: 0.4em;
&.inline {
display: inline-flex;
margin-bottom: 0;
}
&.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.collapsible {
Expand Down
9 changes: 7 additions & 2 deletions client/src/components/History/SwitchToHistoryLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ function viewHistoryInNewTab(history: HistorySummary) {
<template>
<div>
<LoadingSpan v-if="!history" />
<div v-else v-b-tooltip.hover.top.html :title="`<b>${actionText}</b><br>${history.name}`" class="history-link">
<BLink class="truncate" href="#" @click.stop="onClick(history)">
<div v-else class="history-link">
<BLink
v-b-tooltip.hover.top.noninteractive.html
class="truncate"
href="#"
:title="`<b>${actionText}</b><br>${history.name}`"
@click.stop="onClick(history)">
{{ history.name }}
</BLink>

Expand Down
65 changes: 47 additions & 18 deletions client/src/components/Panels/FlexPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { useDebounce, useDraggable } from "@vueuse/core";
import { computed, type PropType, ref, watch } from "vue";
import { computed, ref, watch } from "vue";
import { useTimeoutThrottle } from "@/composables/throttle";
Expand All @@ -13,25 +13,25 @@ const { throttle } = useTimeoutThrottle(10);
library.add(faChevronLeft, faChevronRight);
const props = defineProps({
collapsible: {
type: Boolean,
default: true,
},
side: {
type: String as PropType<"left" | "right">,
default: "right",
},
interface Props {
collapsible?: boolean;
side?: "left" | "right";
minWidth?: number;
maxWidth?: number;
defaultWidth?: number;
}
const props = withDefaults(defineProps<Props>(), {
collapsible: true,
side: "right",
minWidth: 200,
maxWidth: 800,
defaultWidth: 300,
});
const minWidth = 200;
const maxWidth = 800;
const defaultWidth = 300;
const draggable = ref<HTMLElement | null>(null);
const root = ref<HTMLElement | null>(null);
const panelWidth = ref(defaultWidth);
const panelWidth = ref(props.defaultWidth);
const show = ref(true);
const { position, isDragging } = useDraggable(draggable, {
Expand Down Expand Up @@ -79,10 +79,39 @@ watch(position, () => {
const rectRoot = root.value.getBoundingClientRect();
const rectDraggable = draggable.value.getBoundingClientRect();
panelWidth.value = determineWidth(rectRoot, rectDraggable, minWidth, maxWidth, props.side, position.value.x);
panelWidth.value = determineWidth(
rectRoot,
rectDraggable,
props.minWidth,
props.maxWidth,
props.side,
position.value.x
);
});
});
/** If the the `maxWidth` changes, prevent the panel from exceeding it */
watch(
() => props.maxWidth,
(newVal) => {
if (newVal && panelWidth.value > newVal) {
panelWidth.value = props.maxWidth;
}
},
{ immediate: true }
);
/** If the `minWidth` changes, ensure the panel width is at least the `minWidth` */
watch(
() => props.minWidth,
(newVal) => {
if (newVal && panelWidth.value < newVal) {
panelWidth.value = newVal;
}
},
{ immediate: true }
);
function onKeyLeft() {
if (props.side === "left") {
decreaseWidth();
Expand All @@ -100,11 +129,11 @@ function onKeyRight() {
}
function increaseWidth(by = 50) {
panelWidth.value = Math.min(panelWidth.value + by, maxWidth);
panelWidth.value = Math.min(panelWidth.value + by, props.maxWidth);
}
function decreaseWidth(by = 50) {
panelWidth.value = Math.max(panelWidth.value - by, minWidth);
panelWidth.value = Math.max(panelWidth.value - by, props.minWidth);
}
const sideClasses = computed(() => ({
Expand Down
50 changes: 45 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 @@ -219,6 +241,14 @@ 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(() => {
return {
...invocationStep.value.headerClass,
"cursor-pointer": props.isInvocation,
"node-header": !props.isInvocation || invocationStep.value.headerClass === undefined,
"cursor-move": !props.readonly && !props.isInvocation,
};
});
const inputs = computed(() => {
const connections = connectionStore.getConnectionsForStep(props.id);
const extraStepInputs = stepStore.getStepExtraInputs(props.id);
Expand Down Expand Up @@ -247,6 +277,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 @@ -317,12 +348,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
Loading

0 comments on commit 516f541

Please sign in to comment.