mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #16136 from martenson/invo
Add option to see invocations related to a history
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user