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

Workflow Invocation view improvements #18615

Merged
merged 24 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b877fa3
Workflow Invocation view improvements
ahmedhamidawan Jul 29, 2024
53ace3c
move invocation steps to a separate tab, show whole step below
ahmedhamidawan Jul 31, 2024
8028bd7
add workflow input indicator to `WorkflowInvocationStepHeader`
ahmedhamidawan Jul 31, 2024
964e6e1
decrease step header size in `InvocationGraph` to "sm"
ahmedhamidawan Jul 31, 2024
6a4b3b5
add a tab view for jobs under a step in invocation view
ahmedhamidawan Aug 1, 2024
f1b0a74
use `openapi-fetch` for fetching job
ahmedhamidawan Aug 15, 2024
6b764b8
remove redundant props/emits for invocation steps
ahmedhamidawan Aug 15, 2024
ac7428e
convert `ProgressBar.vue` to composition API + ts
ahmedhamidawan Aug 16, 2024
161ee58
do not include `Steps` tab for subworkflows
ahmedhamidawan Aug 25, 2024
c8534bc
add `getContentItemState` function
ahmedhamidawan Sep 3, 2024
83ec705
add job duration, `WorkflowInvocationJob` component
ahmedhamidawan Sep 3, 2024
992319a
check `activeNodeId` for `null` (it can be 0 as well)
ahmedhamidawan Sep 3, 2024
d5228a5
show workflow input value on graph
ahmedhamidawan Sep 3, 2024
4a8bf36
move progress bars to header, shorten `WorkflowRunSuccess`
ahmedhamidawan Sep 20, 2024
940b3c2
do not auto scroll to the clicked step
ahmedhamidawan Sep 20, 2024
f71af2f
add `force` prop to `WorkflowRunButton` for routing
ahmedhamidawan Sep 20, 2024
9756a6a
remove jest since buttons have moved to parent
ahmedhamidawan Sep 21, 2024
33d97f1
add a jest for `JobStep`
ahmedhamidawan Oct 1, 2024
7dda944
adjust seleniums for changes in `JobStep` and invocations in general
ahmedhamidawan Oct 2, 2024
d805c99
add a jest for `SwitchToHistoryLink` that tests current history case
ahmedhamidawan Oct 2, 2024
0463441
do not auto-activate step on invocation view load
ahmedhamidawan Nov 5, 2024
6cff8d6
on a repetition of the step clicked, scroll to the step
ahmedhamidawan Nov 5, 2024
8f6c955
fix failing `WorkflowInvocationHeader` jest
ahmedhamidawan Nov 5, 2024
0c50efc
show `JobMetrics` in workflow invocation job by just using `JobDetails`
ahmedhamidawan Nov 11, 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
3 changes: 3 additions & 0 deletions client/src/api/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ import { type components } from "@/api/schema";

export type JobDestinationParams = components["schemas"]["JobDestinationParams"];
export type ShowFullJobResponse = components["schemas"]["ShowFullJobResponse"];
export type JobBaseModel = components["schemas"]["JobBaseModel"];
export type JobDetails = components["schemas"]["ShowFullJobResponse"] | components["schemas"]["EncodedJobDetails"];
export type JobInputSummary = components["schemas"]["JobInputSummary"];
export type JobDisplayParametersSummary = components["schemas"]["JobDisplayParametersSummary"];
export type JobMetric = components["schemas"]["JobMetric"];
16 changes: 13 additions & 3 deletions client/src/components/Grid/GridInvocation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import { storeToRefs } from "pinia";
import { computed } from "vue";

import type { WorkflowInvocation } from "@/api/invocations";
import invocationsGridConfig from "@/components/Grid/configs/invocations";
import invocationsBatchConfig from "@/components/Grid/configs/invocationsBatch";
import invocationsHistoryGridConfig from "@/components/Grid/configs/invocationsHistory";
import invocationsWorkflowGridConfig from "@/components/Grid/configs/invocationsWorkflow";
import { useUserStore } from "@/stores/userStore";
Expand All @@ -19,19 +21,22 @@ interface Props {
headerMessage?: string;
ownerGrid?: boolean;
filteredFor?: { type: "History" | "StoredWorkflow"; id: string; name: string };
invocationsList?: WorkflowInvocation[];
}

const props = withDefaults(defineProps<Props>(), {
noInvocationsMessage: "No Workflow Invocations to display",
headerMessage: "",
ownerGrid: true,
filteredFor: undefined,
invocationsList: undefined,
});

const { currentUser } = storeToRefs(useUserStore());

const forStoredWorkflow = computed(() => props.filteredFor?.type === "StoredWorkflow");
const forHistory = computed(() => props.filteredFor?.type === "History");
const forBatch = computed(() => !!props.invocationsList?.length);

const effectiveNoInvocationsMessage = computed(() => {
let message = props.noInvocationsMessage;
Expand All @@ -51,6 +56,9 @@ const effectiveTitle = computed(() => {
});

const extraProps = computed(() => {
if (forBatch.value) {
return Object.fromEntries(props.invocationsList.map((invocation) => [invocation.id, invocation]));
}
const params: {
workflow_id?: string;
history_id?: string;
Expand All @@ -72,7 +80,9 @@ const extraProps = computed(() => {
});

let gridConfig: GridConfig;
if (forStoredWorkflow.value) {
if (forBatch.value) {
gridConfig = invocationsBatchConfig;
} else if (forStoredWorkflow.value) {
gridConfig = invocationsWorkflowGridConfig;
} else if (forHistory.value) {
gridConfig = invocationsHistoryGridConfig;
Expand All @@ -97,9 +107,9 @@ function refreshTable() {
:grid-message="props.headerMessage"
:no-data-message="effectiveNoInvocationsMessage"
:extra-props="extraProps"
:embedded="forStoredWorkflow || forHistory">
:embedded="forStoredWorkflow || forHistory || forBatch">
<template v-slot:expanded="{ rowData }">
<span class="float-right position-absolute mr-4" style="right: 0" :data-invocation-id="rowData.id">
<span class="position-absolute ml-4" :data-invocation-id="rowData.id">
<small>
<b>Last updated: <UtcDate :date="rowData.update_time" mode="elapsed" />; Invocation ID:</b>
</small>
Expand Down
80 changes: 80 additions & 0 deletions client/src/components/Grid/configs/invocationsBatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { faEye } from "@fortawesome/free-solid-svg-icons";

import { type WorkflowInvocation } from "@/api/invocations";
import { getAppRoot } from "@/onload";

import { type FieldArray, type GridConfig } from "./types";

/**
* Request and return invocations for the given workflow (and current user) from server
*/
async function getData(
offset: number,
limit: number,
search: string,
sort_by: string,
sort_desc: boolean,
extraProps?: Record<string, unknown>
) {
// extra props will be Record<string, Invocation>; get array of invocations
const data = Object.values(extraProps ?? {}) as WorkflowInvocation[];
const totalMatches = data.length;
return [data, totalMatches];
}

/**
* Declare columns to be displayed
*/
const fields: FieldArray = [
{
key: "expand",
title: null,
type: "expand",
},
{
key: "view",
title: "View",
type: "button",
icon: faEye,
handler: (data) => {
const url = `${getAppRoot()}workflows/invocations/${(data as WorkflowInvocation).id}`;
window.open(url, "_blank");
},
converter: () => "",
},
{
key: "history_id",
title: "History",
type: "history",
},
{
key: "create_time",
title: "Invoked",
type: "date",
},
{
key: "state",
title: "State",
type: "helptext",
converter: (data) => {
const invocation = data as WorkflowInvocation;
return `galaxy.invocations.states.${invocation.state}`;
},
},
];

/**
* Grid configuration
*/
const gridConfig: GridConfig = {
id: "invocations-batch-grid",
fields: fields,
getData: getData,
plural: "Workflow Invocations",
sortBy: "create_time",
sortDesc: true,
sortKeys: [],
title: "Workflow Invocations in Batch",
};

export default gridConfig;
22 changes: 2 additions & 20 deletions client/src/components/History/Content/ContentItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useEventStore } from "@/stores/eventStore";
import { clearDrag } from "@/utils/setDrag";

import { JobStateSummary } from "./Collection/JobStateSummary";
import { HIERARCHICAL_COLLECTION_JOB_STATES, type StateMap, STATES } from "./model/states";
import { getContentItemState, type StateMap, STATES } from "./model/states";

import CollectionDescription from "./Collection/CollectionDescription.vue";
import ContentOptions from "./ContentOptions.vue";
Expand Down Expand Up @@ -135,25 +135,7 @@ const state = computed<keyof StateMap>(() => {
if (props.isPlaceholder) {
return "placeholder";
}
if (props.item.accessible === false) {
return "inaccessible";
}
if (props.item.populated_state === "failed") {
return "failed_populated_state";
}
if (props.item.populated_state === "new") {
return "new_populated_state";
}
if (props.item.job_state_summary) {
for (const jobState of HIERARCHICAL_COLLECTION_JOB_STATES) {
if (props.item.job_state_summary[jobState] > 0) {
return jobState;
}
}
} else if (props.item.state) {
return props.item.state;
}
return "ok";
return getContentItemState(props.item);
});

const dataState = computed(() => {
Expand Down
24 changes: 24 additions & 0 deletions client/src/components/History/Content/model/states.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isHDCA } from "@/api";
import { type components } from "@/api/schema";

type DatasetState = components["schemas"]["DatasetState"];
Expand Down Expand Up @@ -146,3 +147,26 @@ export const HIERARCHICAL_COLLECTION_JOB_STATES = [
"queued",
"new",
] as const;

export function getContentItemState(item: any) {
if (isHDCA(item)) {
if (item.populated_state === "failed") {
return "failed_populated_state";
}
if (item.populated_state === "new") {
return "new_populated_state";
}
if (item.job_state_summary) {
for (const jobState of HIERARCHICAL_COLLECTION_JOB_STATES) {
if (item.job_state_summary[jobState] > 0) {
return jobState;
}
}
}
} else if (item.accessible === false) {
return "inaccessible";
} else if (item.state) {
return item.state;
}
return "ok";
}
24 changes: 24 additions & 0 deletions client/src/components/History/SwitchToHistoryLink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ const selectors = {
historyLink: ".history-link",
} as const;

// Mock the history store to always return the same current history id
jest.mock("@/stores/historyStore", () => {
const originalModule = jest.requireActual("@/stores/historyStore");
return {
...originalModule,
useHistoryStore: () => ({
...originalModule.useHistoryStore(),
currentHistoryId: "current-history-id",
}),
};
});

function mountSwitchToHistoryLinkForHistory(history: HistorySummaryExtended) {
const pinia = createTestingPinia();

Expand Down Expand Up @@ -98,6 +110,18 @@ describe("SwitchToHistoryLink", () => {
await expectOptionForHistory("Switch", history);
});

it("should display the appropriate text when the history is the Current history", async () => {
const history = {
id: "current-history-id",
name: "History Current",
deleted: false,
purged: false,
archived: false,
user_id: "user_id",
} as HistorySummaryExtended;
await expectOptionForHistory("This is your current history", history);
});

it("should display the View option when the history is purged", async () => {
const history = {
id: "purged-history-id",
Expand Down
14 changes: 13 additions & 1 deletion client/src/components/History/SwitchToHistoryLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,20 @@ const actionText = computed(() => {
return "View in new tab";
});

const linkTitle = computed(() => {
if (historyStore.currentHistoryId === props.historyId) {
return "This is your current history";
} else {
return `<b>${actionText.value}</b><br>${history.value?.name}`;
}
});

async function onClick(event: MouseEvent, history: HistorySummary) {
const eventStore = useEventStore();
const ctrlKey = eventStore.isMac ? event.metaKey : event.ctrlKey;
if (!ctrlKey && historyStore.currentHistoryId === history.id) {
return;
}
if (!ctrlKey && canSwitch.value) {
if (props.filters) {
historyStore.applyFilters(history.id, props.filters);
Expand Down Expand Up @@ -78,9 +89,10 @@ function viewHistoryInNewTab(history: HistorySummary) {
<div v-else class="history-link">
<BLink
v-b-tooltip.hover.top.noninteractive.html
data-description="switch to history link"
class="truncate"
href="#"
:title="`<b>${actionText}</b><br>${history.name}`"
:title="linkTitle"
@click.stop="onClick($event, history)">
{{ history.name }}
</BLink>
Expand Down
7 changes: 3 additions & 4 deletions client/src/components/JobInformation/JobInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import HelpText from "components/Help/HelpText";
import { JobDetailsProvider } from "components/providers/JobProvider";
import UtcDate from "components/UtcDate";
import { NON_TERMINAL_STATES } from "components/WorkflowInvocationState/util";
import { formatDuration, intervalToDuration } from "date-fns";
import { computed, ref, watch } from "vue";

import { GalaxyApi } from "@/api";
import { rethrowSimple } from "@/utils/simple-error";

import { getJobDuration } from "./utilities";

import DecodedId from "../DecodedId.vue";
import CodeRow from "./CodeRow.vue";

Expand All @@ -27,9 +28,7 @@ const props = defineProps({
},
});

const runTime = computed(() =>
formatDuration(intervalToDuration({ start: new Date(job.value.create_time), end: new Date(job.value.update_time) }))
);
const runTime = computed(() => getJobDuration(job.value));

const jobIsTerminal = computed(() => job.value && !NON_TERMINAL_STATES.includes(job.value.state));

Expand Down
7 changes: 7 additions & 0 deletions client/src/components/JobInformation/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { formatDuration, intervalToDuration } from "date-fns";

import type { JobBaseModel } from "@/api/jobs";

export function getJobDuration(job: JobBaseModel): string {
return formatDuration(intervalToDuration({ start: new Date(job.create_time), end: new Date(job.update_time) }));
}
17 changes: 15 additions & 2 deletions client/src/components/Panels/InvocationsPanel.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
<script setup lang="ts">
import { BAlert } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { ref } from "vue";

import { useUserStore } from "@/stores/userStore";

import InvocationScrollList from "../Workflow/Invocation/InvocationScrollList.vue";
import ActivityPanel from "./ActivityPanel.vue";

const { currentUser, toggledSideBar } = storeToRefs(useUserStore());

const shouldCollapse = ref(false);
function collapseOnLeave() {
if (shouldCollapse.value) {
toggledSideBar.value = "";
}
}
</script>

<template>
<!-- eslint-disable-next-line vuejs-accessibility/mouse-events-have-key-events -->
<ActivityPanel
title="Workflow Invocations"
go-to-all-title="Open Invocations List"
href="/workflows/invocations"
@goToAll="toggledSideBar = ''">
<InvocationScrollList v-if="currentUser && !currentUser?.isAnonymous" in-panel />
@goToAll="shouldCollapse = true"
@mouseleave.native="collapseOnLeave">
<InvocationScrollList
v-if="currentUser && !currentUser?.isAnonymous"
in-panel
@invocation-clicked="shouldCollapse = true" />
<BAlert v-else variant="info" class="mt-3" show>Please log in to view your Workflow Invocations.</BAlert>
</ActivityPanel>
</template>
Loading
Loading