Skip to content

Commit

Permalink
move invocation steps to a separate tab, show whole step below
Browse files Browse the repository at this point in the history
- Instead of showing steps right next to the invocation graph, moves steps (back) into a separate tab
- To make this work, adds a `graphStepsByStoreId` to the `invocationStore` that helps access the graph steps in multiple components (TODO: improve how this is done)
- Instead of clicking on a node/step in the graph and showing the job for the step that the user clicks, now we show the whole expanded step below
  • Loading branch information
ahmedhamidawan committed Jul 31, 2024
1 parent e752647 commit 63ad93c
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 204 deletions.
7 changes: 1 addition & 6 deletions client/src/components/Workflow/Editor/NodeInvocationText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { isWorkflowInput } from "@/components/Workflow/constants";
import { type GraphStep, iconClasses } from "@/composables/useInvocationGraph";
import { type GraphStep, iconClasses, statePlaceholders } from "@/composables/useInvocationGraph";
const props = defineProps<{
invocationStep: GraphStep;
}>();
const statePlaceholders: Record<string, string> = {
ok: "successful",
error: "failed",
};
</script>
<template>
<div class="p-1 unselectable">
Expand Down
153 changes: 37 additions & 116 deletions client/src/components/Workflow/Invocation/Graph/InvocationGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,21 @@ import {
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { useElementBounding } from "@vueuse/core";
import { BAlert, BButton, BCard, BCardBody, BCardHeader } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed, onUnmounted, ref, watch } from "vue";
import type { WorkflowInvocationElementView } from "@/api/invocations";
import { JobProvider } from "@/components/providers";
import { useDatatypesMapper } from "@/composables/datatypesMapper";
import { useInvocationGraph } from "@/composables/useInvocationGraph";
import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
import type { Workflow } from "@/stores/workflowStore";
import { withPrefix } from "@/utils/redirect";
import WorkflowInvocationSteps from "./WorkflowInvocationSteps.vue";
import Heading from "@/components/Common/Heading.vue";
import ExternalLink from "@/components/ExternalLink.vue";
import JobInformation from "@/components/JobInformation/JobInformation.vue";
import JobParameters from "@/components/JobParameters/JobParameters.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
import FlexPanel from "@/components/Panels/FlexPanel.vue";
import WorkflowGraph from "@/components/Workflow/Editor/WorkflowGraph.vue";
import WorkflowInvocationStep from "@/components/WorkflowInvocationState/WorkflowInvocationStep.vue";
import WorkflowInvocationStepHeader from "@/components/WorkflowInvocationState/WorkflowInvocationStepHeader.vue";
library.add(faArrowDown, faChevronDown, faChevronUp, faSignInAlt, faSitemap, faTimes);
Expand Down Expand Up @@ -70,11 +64,9 @@ const loadingGraph = ref(true);
const initialLoading = ref(true);
const errored = ref(false);
const errorMessage = ref("");
const showingJobId = ref<string | undefined>(undefined);
const pollTimeout = ref<any>(null);
const hideGraph = ref(false);
const jobCard = ref<BCard | null>(null);
const loadedJobInfo = ref<HTMLDivElement | null>(null);
const stepCard = ref<BCard | null>(null);
const loadedJobInfo = ref<typeof WorkflowInvocationStep | null>(null);
const invocationRef = computed(() => props.invocation);
Expand Down Expand Up @@ -125,34 +117,20 @@ watch(
{ immediate: true }
);
// when the graph is hidden/visible, reset the active node and showingJobId
watch(
() => hideGraph.value,
() => {
showingJobId.value = undefined;
activeNodeId.value = null;
}
);
// scroll to the job card when it is loaded (only on invocation route)
// TODO: Maybe do not do this on first load...
if (props.isFullPage) {
watch(
() => loadedJobInfo.value,
async (jobInfo) => {
if (jobInfo) {
scrollJobToView();
scrollStepToView();
}
},
{ immediate: true }
);
}
// properties for handling the flex-draggable steps panel
const invocationContainer = ref<HTMLDivElement | null>(null);
const { width: containerWidth } = useElementBounding(invocationContainer);
const minWidth = computed(() => containerWidth.value * 0.3);
const maxWidth = computed(() => 0.7 * containerWidth.value);
onUnmounted(() => {
clearTimeout(pollTimeout.value);
});
Expand Down Expand Up @@ -196,17 +174,9 @@ async function pollInvocationGraph() {
}
}
function scrollJobToView() {
const jobCardHeader = jobCard.value?.querySelector(".card-header");
jobCardHeader?.scrollIntoView({ behavior: "smooth", block: "start" });
}
function toggleActiveStep(stepId: number) {
if (activeNodeId.value === stepId) {
activeNodeId.value = null;
} else {
activeNodeId.value = stepId;
}
function scrollStepToView() {
const stepCardHeader = stepCard.value?.querySelector(".card-header");
stepCardHeader?.scrollIntoView({ behavior: "smooth", block: "start" });
}
</script>

Expand All @@ -222,8 +192,8 @@ function toggleActiveStep(stepId: number) {
<BAlert v-else show variant="danger"> Unknown Error </BAlert>
</div>
<div v-else-if="steps && datatypesMapper">
<div ref="invocationContainer" class="d-flex">
<div v-if="!hideGraph" class="position-relative w-100">
<div class="d-flex">
<div class="position-relative w-100">
<BCard no-body>
<WorkflowGraph
:steps="steps"
Expand All @@ -235,91 +205,42 @@ function toggleActiveStep(stepId: number) {
is-invocation
readonly />
</BCard>
<BButton
class="position-absolute text-decoration-none m-2"
style="top: 0; right: 0"
data-description="hide invocation graph"
size="sm"
@click="hideGraph = true">
<FontAwesomeIcon :icon="faTimes" class="mr-1" />
<span v-localize>Hide Graph</span>
</BButton>
</div>
<BButton
v-else
v-b-tooltip.noninteractive.hover.right="'Show Graph'"
size="sm"
class="p-0"
style="width: min-content"
@click="hideGraph = false">
<FontAwesomeIcon :icon="faSitemap" />
<div v-localize>Show Graph</div>
</BButton>
<component
:is="!hideGraph ? FlexPanel : 'div'"
v-if="containerWidth"
side="right"
:collapsible="false"
class="ml-2"
:class="{ 'w-100': hideGraph }"
:min-width="minWidth"
:max-width="maxWidth"
:default-width="containerWidth * 0.4">
<WorkflowInvocationSteps
class="graph-steps-aside"
:class="{ 'steps-fixed-height': !hideGraph }"
:steps="steps"
:store-id="storeId"
:invocation="invocationRef"
:workflow="props.workflow"
:is-full-page="props.isFullPage"
:hide-graph="hideGraph"
:showing-job-id="showingJobId || ''"
:active-node-id="activeNodeId !== null ? activeNodeId : undefined"
@update:showing-job-id="(jobId) => (showingJobId = jobId)"
@focus-on-step="toggleActiveStep" />
</component>
</div>
<BCard v-if="!hideGraph" ref="jobCard" class="mt-1" no-body>
<BCardHeader class="d-flex justify-content-between align-items-center">
<Heading inline size="md">
<span v-if="showingJobId">
Showing Job Details for
<ExternalLink :href="withPrefix(`/jobs/${showingJobId}/view`)">
<code>{{ showingJobId }}</code>
</ExternalLink>
</span>
<span v-else>No Job Selected</span>
<BCard ref="stepCard" class="mt-1" no-body>
<BCardHeader
class="d-flex justify-content-between align-items-center"
:class="activeNodeId ? steps[activeNodeId]?.headerClass : ''">
<Heading inline size="md" class="w-100 mr-2">
<WorkflowInvocationStepHeader
v-if="activeNodeId !== null"
class="w-100"
:workflow-step="props.workflow.steps[activeNodeId]"
:graph-step="steps[activeNodeId]"
:invocation-step="props.invocation.steps[activeNodeId]" />
<span v-else>No Step Selected</span>
</Heading>
<div>
<BButton
v-if="showingJobId"
v-b-tooltip.hover.noninteractive
title="Scroll to Job"
@click="scrollJobToView()">
<div class="d-flex">
<BButton v-if="activeNodeId !== null" title="Scroll to Step" @click="scrollStepToView()">
<FontAwesomeIcon :icon="faArrowDown" />
</BButton>
<BButton
v-if="showingJobId"
v-b-tooltip.hover.noninteractive
title="Hide Job"
@click="showingJobId = undefined">
<BButton v-if="activeNodeId !== null" title="Hide Step" @click="activeNodeId = null">
<FontAwesomeIcon :icon="faTimes" />
</BButton>
</div>
</BCardHeader>
<BCardBody>
<JobProvider v-if="showingJobId" :id="showingJobId" v-slot="{ item, loading }">
<BAlert v-if="loading" show>
<LoadingSpan message="Loading Job Information" />
</BAlert>
<div v-else ref="loadedJobInfo">
<JobInformation v-if="item" :job_id="item.id" />
<p></p>
<JobParameters v-if="item" :job-id="item.id" :include-title="false" />
</div>
</JobProvider>
<BAlert v-else show>Select a job from a step in the invocation to view its details here.</BAlert>
<WorkflowInvocationStep
v-if="activeNodeId !== null"
ref="loadedJobInfo"
:key="activeNodeId"
:invocation="props.invocation"
:workflow="props.workflow"
:workflow-step="props.workflow.steps[activeNodeId]"
in-graph-view
:graph-step="steps[activeNodeId]"
expanded />
<BAlert v-else show>Click on a step in the invocation to view its details here.</BAlert>
</BCardBody>
</BCard>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faChevronDown, faChevronUp, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { storeToRefs } from "pinia";
import { computed, ref, watch } from "vue";
import type { WorkflowInvocationElementView } from "@/api/invocations";
import { isWorkflowInput } from "@/components/Workflow/constants";
import type { GraphStep } from "@/composables/useInvocationGraph";
import { useInvocationStore } from "@/stores/invocationStore";
import type { Workflow } from "@/stores/workflowStore";
import WorkflowInvocationStep from "@/components/WorkflowInvocationState/WorkflowInvocationStep.vue";
library.add(faChevronDown, faChevronUp, faSignInAlt);
interface Props {
/** The steps for the invocation graph */
steps: { [index: string]: GraphStep };
/** The store id for the invocation graph */
storeId: string;
/** The invocation to display */
Expand All @@ -42,6 +41,10 @@ const props = withDefaults(defineProps<Props>(), {
activeNodeId: undefined,
});
const invocationStore = useInvocationStore();
const { graphStepsByStoreId } = storeToRefs(invocationStore);
const graphSteps = computed(() => graphStepsByStoreId.value[props.storeId]);
const stepsDiv = ref<HTMLDivElement>();
const expandInvocationInputs = ref(false);
Expand Down Expand Up @@ -79,7 +82,7 @@ function showJob(jobId: string | undefined) {
</script>

<template>
<div ref="stepsDiv" class="d-flex flex-column w-100">
<div v-if="graphSteps" ref="stepsDiv" class="d-flex flex-column w-100">
<!-- Input Steps grouped in a separate portlet -->
<div v-if="workflowInputSteps.length > 1" class="ui-portlet-section w-100">
<div
Expand All @@ -106,7 +109,7 @@ function showJob(jobId: string | undefined) {
:workflow="props.workflow"
:workflow-step="step"
:in-graph-view="!props.hideGraph"
:graph-step="steps[step.id]"
:graph-step="graphSteps[step.id]"
:expanded="props.hideGraph ? undefined : props.activeNodeId === step.id"
:showing-job-id="props.showingJobId"
@show-job="showJob"
Expand All @@ -122,7 +125,7 @@ function showJob(jobId: string | undefined) {
:workflow="props.workflow"
:workflow-step="step"
:in-graph-view="!props.hideGraph"
:graph-step="steps[step.id]"
:graph-step="graphSteps[step.id]"
:expanded="props.hideGraph ? undefined : props.activeNodeId === step.id"
:showing-job-id="props.showingJobId"
@show-job="showJob"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { BAlert, BButton } from "bootstrap-vue";
import { computed } from "vue";
import { type InvocationJobsSummary, type InvocationStep, type WorkflowInvocationElementView } from "@/api/invocations";
import { useWorkflowInstance } from "@/composables/useWorkflowInstance";
import { getRootFromIndexLink } from "@/onload";
import type { Workflow } from "@/stores/workflowStore";
import { withPrefix } from "@/utils/redirect";
import {
Expand Down Expand Up @@ -34,14 +34,13 @@ interface Props {
jobStatesSummary: InvocationJobsSummary;
index?: number;
isSubworkflow?: boolean;
workflow?: Workflow;
}
const props = defineProps<Props>();
const generatePdfTooltip = "Generate PDF report for this workflow invocation";
const { workflow } = useWorkflowInstance(props.invocation?.workflow_id ?? "");
const invocationId = computed<string | undefined>(() => props.invocation?.id);
const indexStr = computed(() => {
Expand Down Expand Up @@ -227,11 +226,11 @@ function onCancel() {
</div>
<!-- An invocation has been loaded, display the graph -->
<div v-if="invocation">
<div v-if="workflow && !isSubworkflow">
<div v-if="props.workflow && !isSubworkflow">
<InvocationGraph
class="mt-1"
:invocation="invocation"
:workflow="workflow"
:workflow="props.workflow"
:is-terminal="invocationAndJobTerminal"
:is-scheduled="invocationSchedulingTerminal"
:is-full-page="isFullPage"
Expand Down
Loading

0 comments on commit 63ad93c

Please sign in to comment.