Merge pull request #16136 from martenson/invo

Add option to see invocations related to a history
This commit is contained in:
Marius van den Beek
2023-05-28 11:19:30 +02:00
committed by GitHub
13 changed files with 216 additions and 56 deletions
@@ -15,6 +15,7 @@ const expectedOptions = [
"Export Tool Citations",
"Export History to File",
"Extract Workflow",
"Show Invocations",
"Share or Publish",
"Set Permissions",
"Make Private",
@@ -104,6 +104,14 @@
<span v-localize>Extract Workflow</span>
</b-dropdown-item>
<b-dropdown-item
:disabled="isAnonymous"
:title="userTitle('Display Workflow Invocations')"
@click="$router.push(`/histories/${history.id}/invocations`)">
<Icon fixed-width icon="sitemap" class="fa-rotate-270 mr-1" />
<span v-localize>Show Invocations</span>
</b-dropdown-item>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item
@@ -6,7 +6,7 @@
class="sharing-indicator-published"
size="sm"
variant="link"
:title="'Search more published workflows' | localize"
:title="'Search more published items' | localize"
@click.prevent="$emit('filter', 'is:published')">
<Icon fixed-width icon="globe" />
</b-button>
@@ -16,7 +16,7 @@
class="sharing-indicator-shared"
size="sm"
variant="link"
:title="'Search more workflows shared with me' | localize"
:title="'Search more items shared with me' | localize"
@click.prevent="$emit('filter', 'is:shared_with_me')">
<Icon fixed-width icon="share-alt" />
</b-button>
+15 -16
View File
@@ -6,30 +6,32 @@
<sts-download-button
v-if="effectiveExportLink"
class="float-right markdown-pdf-export"
variant="link"
:fallback-url="exportLink"
:download-endpoint="downloadEndpoint"
title="Download PDF"></sts-download-button>
size="sm"
title="Generate PDF">
</sts-download-button>
<b-button
v-if="!readOnly"
v-b-tooltip.hover.bottom
class="float-right markdown-edit"
title="Edit Markdown"
variant="link"
v-b-tooltip.hover
class="float-right markdown-edit mr-2"
role="button"
size="sm"
title="Edit Markdown"
@click="$emit('onEdit')">
Edit
<font-awesome-icon icon="edit" />
</b-button>
<h1 class="float-right align-middle mr-1 mt-2 h-md">Galaxy {{ markdownConfig.model_class }}</h1>
<span class="float-left font-weight-light mb-3">
<small class="text-break">Title: {{ markdownConfig.title || markdownConfig.model_class }}</small>
<br />
<small>Created by {{ markdownConfig.username }}</small>
<h1 class="float-right align-middle mr-2 mt-1 h-md">Galaxy {{ markdownConfig.model_class }}</h1>
<span class="float-left font-weight-light">
<h1 class="text-break align-middle">
Title: {{ markdownConfig.title || markdownConfig.model_class }}
</h1>
</span>
</div>
<b-badge variant="info" class="w-100 rounded mb-3 white-space-normal">
<div class="float-left m-1 text-break">Published with Galaxy {{ version }} on {{ time }}</div>
<div class="float-right m-1">Identifier {{ markdownConfig.id }}</div>
<div class="float-left m-1 text-break">Generated with Galaxy {{ version }} on {{ time }}</div>
<div class="float-right m-1">Identifier: {{ markdownConfig.id }}</div>
</b-badge>
<div>
<b-alert v-if="markdownErrors.length > 0" variant="warning" show>
@@ -242,9 +244,6 @@ export default {
content: content,
};
},
onDownload() {
window.location.href = this.exportLink;
},
},
};
</script>
@@ -47,10 +47,12 @@ describe("StsDownloadButton", () => {
});
it("should fallback to a URL if tasks not enabled", async () => {
const windowSpy = jest.spyOn(window, "open");
windowSpy.mockImplementation(() => {});
const wrapper = await mountStsDownloadButtonWrapper(NO_TASKS_CONFIG);
wrapper.vm.onDownload(NO_TASKS_CONFIG);
await flushPromises();
expect(window.location).toBeAt(FALLBACK_URL);
expect(window.open).toBeCalled();
});
it("should poll until ready", async () => {
+7 -1
View File
@@ -5,8 +5,10 @@
v-b-tooltip.hover.bottom
:title="title"
:variant="variant"
:size="size"
role="button"
@click="onDownload(config)">
Generate
<font-awesome-icon v-if="waiting" icon="spinner" spin />
<font-awesome-icon v-else icon="download" />
</b-button>
@@ -57,6 +59,10 @@ export default {
type: String,
default: null,
},
size: {
type: String,
default: "md",
},
},
data() {
return {
@@ -76,7 +82,7 @@ export default {
},
onDownload(config) {
if (!config.enable_celery_tasks) {
window.location.assign(withPrefix(this.fallbackUrl));
window.open(withPrefix(this.fallbackUrl));
} else {
this.waiting = true;
axios
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { computed } from "vue";
import InvocationsList from "@/components/Workflow/InvocationsList.vue";
import { storeToRefs } from "pinia";
import { useUserStore } from "@/stores/userStore";
import { useHistoryStore } from "@/stores/historyStore";
interface HistoryInvocationProps {
historyId: string;
}
const props = defineProps<HistoryInvocationProps>();
const { currentUser } = storeToRefs(useUserStore());
const { getHistoryNameById } = useHistoryStore();
const historyName = computed(() => getHistoryNameById(props.historyId));
</script>
<template>
<div>
<InvocationsList
v-if="currentUser && historyName"
:user-id="currentUser.id"
:history-id="historyId"
:history-name="historyName" />
</div>
</template>
@@ -5,6 +5,7 @@
:markdown-config="markdownConfig"
:enable_beta_markdown_export="config.enable_beta_markdown_export"
:export-link="exportUrl"
:download-endpoint="stsUrl(config)"
@onEdit="onEdit" />
</config-provider>
</template>
@@ -60,6 +61,9 @@ export default {
onEdit() {
window.location = withPrefix(`/pages/create?invocation_id=${this.invocationId}`);
},
stsUrl(config) {
return `${this.dataUrl}/prepare_download`;
},
},
};
</script>
@@ -66,7 +66,11 @@ describe("InvocationsList.vue", () => {
describe("for a workflow with an empty invocation list", () => {
beforeEach(async () => {
axiosMock.onAny().reply(200, [], { total_matches: "0" });
axiosMock
.onGet("/api/invocations", {
params: { limit: 50, offset: 0, include_terminal: false, workflow_id: "abcde145678" },
})
.reply(200, [], { total_matches: "0" });
const propsData = {
ownerGrid: false,
storedWorkflowName: "My Workflow",
@@ -80,12 +84,47 @@ describe("InvocationsList.vue", () => {
});
it("title should be shown", async () => {
expect(wrapper.find("#invocations-title").text()).toBe("Workflow Invocations for My Workflow");
expect(wrapper.find("#invocations-title").text()).toBe('Workflow Invocations for workflow "My Workflow"');
});
it("no invocations message should be shown when not loading", async () => {
expect(wrapper.find("#no-invocations").exists()).toBe(true);
});
it("should not render pager", async () => {
expect(wrapper.find(".gx-invocations-grid-pager").exists()).toBeFalsy();
});
});
describe("for a history with an empty invocation list", () => {
beforeEach(async () => {
axiosMock
.onGet("/api/invocations", {
params: { limit: 50, offset: 0, include_terminal: false, history_id: "abcde145678" },
})
.reply(200, [], { total_matches: "0" });
const propsData = {
ownerGrid: false,
historyName: "My History",
historyId: "abcde145678",
};
wrapper = mount(InvocationsList, {
propsData,
localVue,
});
});
it("title should be shown", async () => {
expect(wrapper.find("#invocations-title").text()).toBe('Workflow Invocations for history "My History"');
});
it("no invocations message should be shown when not loading", async () => {
expect(wrapper.find("#no-invocations").exists()).toBe(true);
});
it("should not render pager", async () => {
expect(wrapper.find(".gx-invocations-grid-pager").exists()).toBeFalsy();
});
});
describe("with invocation", () => {
@@ -132,11 +171,8 @@ describe("InvocationsList.vue", () => {
expect(columns.at(3).text()).toBe(
formatDistanceToNow(parseISO(`${mockInvocationData.create_time}Z`), { addSuffix: true })
);
expect(columns.at(4).text()).toBe(
formatDistanceToNow(parseISO(`${mockInvocationData.update_time}Z`), { addSuffix: true })
);
expect(columns.at(5).text()).toBe("scheduled");
expect(columns.at(6).text()).toBe("");
expect(columns.at(4).text()).toBe("scheduled");
expect(columns.at(5).text()).toBe("");
});
it("toggles detail rendering", async () => {
@@ -161,5 +197,45 @@ describe("InvocationsList.vue", () => {
await wrapper.find(".workflow-run").trigger("click");
expect(window.location).toBeAt("workflows/run?id=workflowId");
});
it("should not render pager", async () => {
expect(wrapper.find(".gx-invocations-grid-pager").exists()).toBeFalsy();
});
});
describe("paginations", () => {
beforeEach(async () => {
axiosMock
.onGet("/api/invocations", { params: { limit: 1, offset: 0, include_terminal: false } })
.reply(200, [mockInvocationData], { total_matches: "3" });
const propsData = {
ownerGrid: false,
loading: false,
defaultPerPage: 1,
};
wrapper = mount(InvocationsList, {
propsData,
computed: {
getWorkflowNameByInstanceId: (state) => (id) => "workflow name",
getWorkflowByInstanceId: (state) => (id) => {
return { id: "workflowId" };
},
getHistoryById: (state) => (id) => {
return { id: "historyId" };
},
getHistoryNameById: () => () => "history name",
},
stubs: {
"workflow-invocation-state": {
template: "<span/>",
},
},
localVue,
});
});
it("title should render pager", async () => {
expect(wrapper.find(".gx-invocations-grid-pager").exists()).toBeTruthy();
});
});
});
@@ -6,13 +6,12 @@
<b-alert v-if="headerMessage" variant="info" show>
{{ headerMessage }}
</b-alert>
<b-alert class="index-grid-message" :variant="messageVariant" :show="showMessage">{{ message }}</b-alert>
<b-alert v-bind="alertAttrs">{{ message }}</b-alert>
<b-table
v-bind="indexTableAttrs"
v-model="invocationItemsModel"
no-sort-reset
:fields="invocationFields"
:items="provider"
class="invocations-table">
<template v-slot:empty>
<loading-span v-if="loading" message="Loading workflow invocations" />
@@ -23,7 +22,10 @@
<template v-slot:row-details="row">
<b-card>
<small class="float-right" :data-invocation-id="row.item.id">
<b>Invocation: {{ row.item.id }}</b>
<b>Last updated: <UtcDate :date="row.item.update_time" mode="elapsed" />;</b>
<b
>Invocation ID: <code>{{ row.item.id }}</code></b
>
</small>
<workflow-invocation-state :invocation-id="row.item.id" @invocation-cancelled="refresh" />
</b-card>
@@ -32,14 +34,14 @@
<b-link
v-if="!data.detailsShowing"
v-b-tooltip.hover.top
title="Show Invocation Details"
class="btn-sm fa fa-chevron-down toggle-invocation-details"
title="Show Details"
class="btn-sm fa fa-lg fa-chevron-down toggle-invocation-details"
@click.stop="swapRowDetails(data)" />
<b-link
v-if="data.detailsShowing"
v-b-tooltip.hover.top
title="Hide Invocation Details"
class="btn-sm fa fa-chevron-up toggle-invocation-details"
title="Hide Details"
class="btn-sm fa fa-lg fa-chevron-up toggle-invocation-details"
@click.stop="swapRowDetails(data)" />
</template>
<template v-slot:cell(workflow_id)="data">
@@ -52,7 +54,7 @@
<template v-slot:cell(history_id)="data">
<div
v-b-tooltip.hover.top.html
:title="`<b>Switch to History:</b><br>${getHistoryNameById(data.item.history_id)}`"
:title="`<b>Switch to</b><br>${getHistoryNameById(data.item.history_id)}`"
class="truncate">
<b-link id="switch-to-history" href="#" @click.stop="switchHistory(data.item.history_id)">
<b>{{ getHistoryNameById(data.item.history_id) }}</b>
@@ -73,7 +75,7 @@
</template>
</b-table>
<b-pagination
v-show="rows >= perPage"
v-if="rows >= perPage"
v-model="currentPage"
class="gx-invocations-grid-pager"
v-bind="paginationAttrs"></b-pagination>
@@ -82,7 +84,6 @@
<script>
import { mapActions, mapState } from "pinia";
import { getAppRoot } from "onload/loadConfig";
import { useHistoryStore } from "@/stores/historyStore";
import { getGalaxyInstance } from "app";
@@ -105,26 +106,30 @@ export default {
headerMessage: { type: String, default: "" },
ownerGrid: { type: Boolean, default: true },
userId: { type: String, default: null },
historyId: { type: String, default: null },
historyName: { type: String, default: null },
storedWorkflowId: { type: String, default: null },
storedWorkflowName: { type: String, default: null },
},
data() {
const fields = [
{ key: "expand", label: "", class: "col-button" },
{ key: "workflow_id", label: "Workflow", class: "col-name" },
{ key: "history_id", label: "History", class: "col-history" },
const fields = [{ key: "expand", label: "", class: "col-button" }];
if (!this.storedWorkflowId) {
fields.push({ key: "workflow_id", label: "Workflow", class: "col-name" });
}
if (!this.historyId) {
fields.push({ key: "history_id", label: "History", class: "col-history" });
}
fields.push(
{ key: "create_time", label: "Invoked", class: "col-small", sortable: true },
{ key: "update_time", label: "Updated", class: "col-small", sortable: true },
{ key: "state", class: "col-small" },
{ key: "execute", label: "", class: "col-button" },
];
{ key: "execute", label: "Run", class: "col-button" }
);
return {
tableId: "invocation-list-table",
invocationItems: [],
dataProvider: invocationsProvider,
invocationItemsModel: [],
invocationFields: fields,
perPage: this.rowsPerPage(50),
root: getAppRoot(),
perPage: this.rowsPerPage(this.defaultPerPage || 50),
};
},
computed: {
@@ -137,7 +142,10 @@ export default {
title() {
let title = `Workflow Invocations`;
if (this.storedWorkflowName) {
title += ` for ${this.storedWorkflowName}`;
title += ` for workflow "${this.storedWorkflowName}"`;
}
if (this.historyName) {
title += ` for history "${this.historyName}"`;
}
return title;
},
@@ -146,11 +154,27 @@ export default {
if (this.storedWorkflowName) {
message += ` for ${this.storedWorkflowName}`;
}
if (this.historyName) {
message += ` for ${this.historyName}`;
}
return message;
},
dataProviderParameters() {
const extraParams = this.ownerGrid ? {} : { include_terminal: false };
if (this.storedWorkflowId) {
extraParams["workflow_id"] = this.storedWorkflowId;
}
if (this.historyId) {
extraParams["history_id"] = this.historyId;
}
if (this.userId) {
extraParams["user_id"] = this.userId;
}
return extraParams;
},
},
watch: {
invocationItems: function (invocations) {
items: function (invocations) {
if (invocations) {
const historyIds = new Set();
const workflowIds = new Set();
@@ -175,6 +199,9 @@ export default {
if (this.storedWorkflowId) {
extraParams["workflow_id"] = this.storedWorkflowId;
}
if (this.historyId) {
extraParams["history_id"] = this.historyId;
}
if (this.userId) {
extraParams["user_id"] = this.userId;
}
@@ -2,14 +2,17 @@
<div class="mb-3 workflow-invocation-state-component">
<div v-if="invocationAndJobTerminal">
<span>
<a class="invocation-report-link" :href="invocationLink">
<b>View Report {{ indexStr }}</b>
</a>
<a
v-b-tooltip
class="fa fa-print ml-1 invocation-pdf-link"
<b-button v-b-tooltip.hover size="sm" class="invocation-report-link" :href="invocationLink">
View Report
</b-button>
<b-button
v-b-tooltip.hover
size="sm"
class="invocation-pdf-link"
:href="invocationPdfLink"
title="Download PDF" />
target="_blank">
Generate PDF
</b-button>
</span>
</div>
<div v-else-if="!invocationSchedulingTerminal">
+6
View File
@@ -34,6 +34,7 @@ import HistoryImport from "components/HistoryImport";
import HistoryView from "components/History/HistoryView";
import HistoryPublished from "components/History/HistoryPublished";
import HistoryPublishedList from "components/History/HistoryPublishedList";
import HistoryInvocations from "components/Workflow/HistoryInvocations";
import HistoryMultipleView from "components/History/Multiple/MultipleView";
import InteractiveTools from "components/InteractiveTools/InteractiveTools";
import InvocationReport from "components/Workflow/InvocationReport";
@@ -267,6 +268,11 @@ export function getRouter(Galaxy) {
},
props: true,
},
{
path: "histories/:historyId/invocations",
component: HistoryInvocations,
props: true,
},
{
path: "interactivetool_entry_points/list",
component: InteractiveTools,
+1
View File
@@ -237,6 +237,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs):
webapp.add_client_route("/histories/list")
webapp.add_client_route("/histories/import")
webapp.add_client_route("/histories/{history_id}/export")
webapp.add_client_route("/histories/{history_id}/invocations")
webapp.add_client_route("/histories/list_published")
webapp.add_client_route("/histories/list_shared")
webapp.add_client_route("/histories/rename")