Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualizing workflow runs with an invocation graph view #17413

Merged
merged 42 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6b6bcf5
Add an invocation graph view to the invocation summary
ahmedhamidawan Apr 11, 2024
55dd048
add `implicit_collection_jobs_id` to invocation step summary
ahmedhamidawan Apr 11, 2024
90d955a
make graph and steps appear fixed side by side, improve job display
ahmedhamidawan Apr 16, 2024
ef9ab7b
handle skipped/conditional steps in invocation graph view
ahmedhamidawan Apr 16, 2024
0991cba
improve styling of `View Report` & `Generate PDF` buttons
ahmedhamidawan Apr 16, 2024
1f4f905
route to invocation view instead of expanding within invocation list
ahmedhamidawan Apr 16, 2024
6b8a398
change `skipped` state color to a lighter shade
ahmedhamidawan Apr 16, 2024
a6530af
refactor `WorkflowInvocationState` and improve its jest
ahmedhamidawan Apr 17, 2024
37c4976
fix jest fails caused by modifications made for invocation graph
ahmedhamidawan Apr 17, 2024
d5c0650
fix selenium by only hiding `label` in `NodeInput` with `props.blank`
ahmedhamidawan Apr 18, 2024
b0e93d6
render parts of `InvocationGraph` only if `Summary` tab is active
ahmedhamidawan Apr 19, 2024
1d782a8
do not default-expand outputs for `dataStep` in graph view
ahmedhamidawan Apr 23, 2024
cbd38f4
do not show tooltip labels for output nodes in invocation graph
ahmedhamidawan Apr 23, 2024
1cc5912
remove hover bg-success for output nodes in invocation graph
ahmedhamidawan Apr 23, 2024
39935d6
move steps to `WorkflowInvocationSteps` in a card; improve styling
ahmedhamidawan Apr 23, 2024
6218e31
fix `activeNodeId` not expanding step if it is = 0
ahmedhamidawan Apr 24, 2024
85f6cff
load full workflow for the version that was run
ahmedhamidawan Apr 24, 2024
d3e8ed2
add an invocation view button to individual `InvocationsList`
ahmedhamidawan Apr 24, 2024
a11f21c
center tooltip for `SwitchToHistoryLink`
ahmedhamidawan Apr 24, 2024
8544dbf
add fixed header to `WorkflowInvocationState`, scroll job to view
ahmedhamidawan Apr 24, 2024
12fec54
add `isFullPage` prop instead of checking route
ahmedhamidawan Apr 25, 2024
345e502
rename Summary to Overview, remove Steps tab, add tabs for params
ahmedhamidawan Apr 25, 2024
b6d873c
show invocation count in invocation state view as well
ahmedhamidawan Apr 25, 2024
680bbf1
rename `showStep` to `showJob`
ahmedhamidawan Apr 25, 2024
22f1a16
always toggle a non-input step in graph view
ahmedhamidawan Apr 25, 2024
544048b
make it obvious that jobs are clickable and viewable at the bottom
ahmedhamidawan Apr 25, 2024
c7b3caf
only expand last step if the invocation is terminal
ahmedhamidawan Apr 25, 2024
70bc123
add tooltip to `ShowGraph` button
ahmedhamidawan Apr 25, 2024
87461f5
add icon for queued state in invocation graph
ahmedhamidawan Apr 25, 2024
97f8401
change card to div for invocation view header and steps
ahmedhamidawan Apr 26, 2024
65c0357
if invocation tab is scrollable, add a `pr-2` for scrollbar
ahmedhamidawan Apr 26, 2024
eb0d5e4
change history on invocation success without reloading
ahmedhamidawan Apr 26, 2024
7769792
clear shown job regardless of `props.fullPage`
ahmedhamidawan Apr 26, 2024
45dc2df
use `FlexPanel` for steps in `InvocationGraph`
ahmedhamidawan Apr 26, 2024
cae8c5c
fix bugs with `stateStore.activeNodeId` not syncing
ahmedhamidawan Apr 26, 2024
aaae33d
fix API model imports, remove unused function from `useInvocationGraph`
ahmedhamidawan Apr 27, 2024
20240cd
try to fix scroll-to by using `scrollTo` instead of `scrollIntoView`
ahmedhamidawan Apr 27, 2024
737eb87
fix selenium so that it collapses graph to expand step, fix height of…
ahmedhamidawan Apr 27, 2024
5d623af
Update client/src/components/Workflow/Editor/Node.vue
ahmedhamidawan Apr 29, 2024
0ce7488
change `headerClass` to an object instead of a string
ahmedhamidawan Apr 29, 2024
d3b11e6
Merge remote-tracking branch 'upstream/dev' into graph_view_invocations
dannon Apr 30, 2024
2e53cb7
Minor fixes to Heading component; correct imports and default props.
dannon Apr 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading