mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-25 00:12:08 +08:00
Merge branch 'release_24.2' into dev
This commit is contained in:
@@ -2,26 +2,28 @@ name: Update PR title
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
branches:
|
||||
- "release_**"
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
jobs:
|
||||
update-title:
|
||||
if: github.event.action != 'edited' || github.event.changes.base.ref.from != ''
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Update PR title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
TARGET_BRANCH: "${{ github.base_ref }}"
|
||||
PR_TITLE: "${{ github.event.pull_request.title }}"
|
||||
REPO: "${{ github.repository }}"
|
||||
run: |
|
||||
VERSION=$(echo $TARGET_BRANCH | grep -oP '\d+\.\d+')
|
||||
if [[ -n "$VERSION" && ! "$PR_TITLE" =~ ^\[$VERSION\] ]]; then
|
||||
NEW_TITLE="[$VERSION] $PR_TITLE"
|
||||
gh pr edit $PR_NUMBER --title "$NEW_TITLE"
|
||||
VERSION=$(echo $TARGET_BRANCH | grep -oP '^release_\K\d+.\d+$' || true)
|
||||
NEW_TITLE=$(echo "$PR_TITLE" | sed -E "s/\[[0-9]+\.[0-9]+\] //")
|
||||
if [[ -n "$VERSION" ]]; then
|
||||
NEW_TITLE="[$VERSION] $NEW_TITLE"
|
||||
fi
|
||||
if [[ "$NEW_TITLE" != "$PR_TITLE" ]]; then
|
||||
gh pr edit $PR_NUMBER --repo "$REPO" --title "$NEW_TITLE"
|
||||
fi
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import { BProgress, BProgressBar } from "bootstrap-vue";
|
||||
|
||||
interface Props {
|
||||
total: number;
|
||||
total?: number;
|
||||
note: string;
|
||||
loading: boolean;
|
||||
loading?: boolean;
|
||||
okCount?: number;
|
||||
runningCount?: number;
|
||||
newCount?: number;
|
||||
|
||||
@@ -118,7 +118,7 @@ const showHelpForum = computed(() => isConfigLoaded.value && config.value.enable
|
||||
|
||||
<template>
|
||||
<div class="position-relative">
|
||||
<div class="underlay sticky-top" />
|
||||
<div class="ui-form-header-underlay sticky-top" />
|
||||
<div class="tool-header sticky-top bg-secondary px-2 py-1 rounded">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="py-1 d-flex flex-wrap flex-gapx-1">
|
||||
@@ -209,19 +209,6 @@ const showHelpForum = computed(() => isConfigLoaded.value && config.value.enable
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "scss/theme/blue.scss";
|
||||
|
||||
.underlay::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -$margin-h;
|
||||
left: -0.5rem;
|
||||
right: -0.5rem;
|
||||
height: 50px;
|
||||
background: linear-gradient($white 75%, change-color($white, $alpha: 0));
|
||||
}
|
||||
|
||||
.fa-wrench {
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ export default {
|
||||
ensureParametersSet();
|
||||
stateStore.activeNodeId = null;
|
||||
activityBar.value?.setActiveSideBar("workflow-editor-attributes");
|
||||
if (args.highlight) {
|
||||
if (args?.highlight) {
|
||||
this.highlightAttribute = args.highlight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +327,7 @@ Examples of RDM repositories include [Zenodo](https://zenodo.org/), [Invenio RDM
|
||||
<BCard
|
||||
v-for="plugin in exportPlugins"
|
||||
:key="plugin.id"
|
||||
:data-invocation-export-type="plugin.id"
|
||||
class="wizard-selection-card"
|
||||
:border-variant="exportData.exportPluginFormat === plugin.id ? 'primary' : 'default'"
|
||||
@click="exportData.exportPluginFormat = plugin.id">
|
||||
@@ -350,6 +351,7 @@ Examples of RDM repositories include [Zenodo](https://zenodo.org/), [Invenio RDM
|
||||
<BCard
|
||||
v-for="target in exportDestinationTargets"
|
||||
:key="target.destination"
|
||||
:data-invocation-export-destination="target.destination"
|
||||
:border-variant="exportData.destination === target.destination ? 'primary' : 'default'"
|
||||
:header-bg-variant="exportData.destination === target.destination ? 'primary' : 'default'"
|
||||
:header-text-variant="exportData.destination === target.destination ? 'white' : 'default'"
|
||||
|
||||
@@ -34,8 +34,6 @@ interface Props {
|
||||
workflow: Workflow;
|
||||
/** Whether the invocation is terminal */
|
||||
isTerminal: boolean;
|
||||
/** Whether the invocation is scheduled */
|
||||
isScheduled: boolean;
|
||||
/** The zoom level for the graph */
|
||||
zoom?: number;
|
||||
/** Whether to show the minimap */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="isConfigLoaded">
|
||||
<div class="ui-form-header-underlay sticky-top" />
|
||||
<div v-if="isConfigLoaded" class="sticky-top">
|
||||
<BAlert v-if="!canRunOnHistory" variant="warning" show>
|
||||
<span v-localize>
|
||||
The workflow cannot run because the current history is immutable. Please select a different history
|
||||
|
||||
@@ -66,17 +66,13 @@ const localVue = getLocalVue();
|
||||
* @param version The version of the component to mount (`run_form` or `invocation` view)
|
||||
* @param ownsWorkflow Whether the user owns the workflow associated with the invocation
|
||||
* @param unimportableWorkflow Whether the workflow import should fail
|
||||
* @returns The wrapper object, and the mockRouter object
|
||||
* @returns The wrapper object
|
||||
*/
|
||||
async function mountWorkflowNavigationTitle(
|
||||
version: "run_form" | "invocation",
|
||||
ownsWorkflow = true,
|
||||
unimportableWorkflow = false
|
||||
) {
|
||||
const mockRouter = {
|
||||
push: jest.fn(),
|
||||
};
|
||||
|
||||
let workflowId: string;
|
||||
let invocation;
|
||||
if (version === "invocation") {
|
||||
@@ -96,9 +92,6 @@ async function mountWorkflowNavigationTitle(
|
||||
workflowId,
|
||||
},
|
||||
localVue,
|
||||
mocks: {
|
||||
$router: mockRouter,
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
|
||||
@@ -107,7 +100,7 @@ async function mountWorkflowNavigationTitle(
|
||||
username: ownsWorkflow ? WORKFLOW_OWNER : OTHER_USER,
|
||||
});
|
||||
|
||||
return { wrapper, mockRouter };
|
||||
return { wrapper };
|
||||
}
|
||||
|
||||
describe("WorkflowNavigationTitle renders", () => {
|
||||
@@ -116,10 +109,9 @@ describe("WorkflowNavigationTitle renders", () => {
|
||||
|
||||
const heading = wrapper.find(SELECTORS.WORKFLOW_HEADING);
|
||||
expect(heading.text()).toContain(`Invoked Workflow: ${SAMPLE_WORKFLOW.name}`);
|
||||
expect(heading.text()).toContain(`(version: ${SAMPLE_WORKFLOW.version + 1})`);
|
||||
expect(heading.text()).toContain(`(Version: ${SAMPLE_WORKFLOW.version + 1})`);
|
||||
|
||||
const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);
|
||||
const runButton = actionsGroup.find(SELECTORS.ROUTE_TO_RUN_BUTTON);
|
||||
const runButton = wrapper.find(SELECTORS.ROUTE_TO_RUN_BUTTON);
|
||||
expect(runButton.attributes("title")).toContain("Rerun");
|
||||
expect(runButton.attributes("title")).toContain(SAMPLE_WORKFLOW.name);
|
||||
});
|
||||
@@ -129,24 +121,19 @@ describe("WorkflowNavigationTitle renders", () => {
|
||||
|
||||
const heading = wrapper.find(SELECTORS.WORKFLOW_HEADING);
|
||||
expect(heading.text()).toContain(`Workflow: ${SAMPLE_WORKFLOW.name}`);
|
||||
expect(heading.text()).toContain(`(version: ${SAMPLE_WORKFLOW.version + 1})`);
|
||||
expect(heading.text()).toContain(`(Version: ${SAMPLE_WORKFLOW.version + 1})`);
|
||||
|
||||
const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);
|
||||
const runButton = actionsGroup.find(SELECTORS.EXECUTE_WORKFLOW_BUTTON);
|
||||
const runButton = wrapper.find(SELECTORS.EXECUTE_WORKFLOW_BUTTON);
|
||||
expect(runButton.attributes("title")).toContain("Run");
|
||||
});
|
||||
|
||||
it("edit button if user owns the workflow", async () => {
|
||||
async function findAndClickEditButton(version: "invocation" | "run_form") {
|
||||
const { wrapper, mockRouter } = await mountWorkflowNavigationTitle(version);
|
||||
const { wrapper } = await mountWorkflowNavigationTitle(version);
|
||||
const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);
|
||||
|
||||
const editButton = actionsGroup.find(SELECTORS.EDIT_WORKFLOW_BUTTON);
|
||||
await editButton.trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.push).toHaveBeenCalledWith(
|
||||
expect(editButton.attributes("to")).toBe(
|
||||
`/workflows/edit?id=${SAMPLE_WORKFLOW.id}&version=${SAMPLE_WORKFLOW.version}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { BAlert, BButton, BButtonGroup } from "bootstrap-vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import { isRegisteredUser } from "@/api";
|
||||
import type { WorkflowInvocationElementView } from "@/api/invocations";
|
||||
@@ -21,8 +20,6 @@ import AsyncButton from "../Common/AsyncButton.vue";
|
||||
import ButtonSpinner from "../Common/ButtonSpinner.vue";
|
||||
import WorkflowRunButton from "./WorkflowRunButton.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
interface Props {
|
||||
invocation?: WorkflowInvocationElementView;
|
||||
workflowId: string;
|
||||
@@ -98,16 +95,14 @@ const workflowImportTitle = computed(() => {
|
||||
|
||||
<BAlert v-if="error" variant="danger" show>{{ error }}</BAlert>
|
||||
|
||||
<div class="position-relative">
|
||||
<div v-if="workflow" class="ui-portlet-section">
|
||||
<div class="d-flex portlet-header align-items-center">
|
||||
<div class="position-relative mb-2">
|
||||
<div v-if="workflow" class="bg-secondary px-2 py-1 rounded">
|
||||
<div class="d-flex align-items-center flex-gapx-1">
|
||||
<div class="flex-grow-1" data-description="workflow heading">
|
||||
<div class="px-1">
|
||||
<FontAwesomeIcon :icon="faSitemap" />
|
||||
<b class="mx-1">
|
||||
{{ props.invocation ? "Invoked " : "" }}Workflow: {{ getWorkflowName() }}
|
||||
</b>
|
||||
<i>(version: {{ workflow.version + 1 }})</i>
|
||||
<div>
|
||||
<FontAwesomeIcon :icon="faSitemap" fixed-width />
|
||||
<b> {{ props.invocation ? "Invoked " : "" }}Workflow: {{ getWorkflowName() }} </b>
|
||||
<span>(Version: {{ workflow.version + 1 }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<BButtonGroup>
|
||||
@@ -122,7 +117,7 @@ const workflowImportTitle = computed(() => {
|
||||
"
|
||||
variant="link"
|
||||
:disabled="workflow.deleted"
|
||||
@click="router.push(`/workflows/edit?id=${workflow.id}&version=${workflow.version}`)">
|
||||
:to="`/workflows/edit?id=${workflow.id}&version=${workflow.version}`">
|
||||
<FontAwesomeIcon :icon="faEdit" fixed-width />
|
||||
</BButton>
|
||||
<AsyncButton
|
||||
@@ -138,30 +133,30 @@ const workflowImportTitle = computed(() => {
|
||||
</AsyncButton>
|
||||
|
||||
<slot name="workflow-title-actions" />
|
||||
|
||||
<ButtonSpinner
|
||||
v-if="!props.invocation"
|
||||
id="run-workflow"
|
||||
data-description="execute workflow button"
|
||||
:wait="runWaiting"
|
||||
:disabled="runDisabled"
|
||||
size="sm"
|
||||
title="Run"
|
||||
@onClick="emit('on-execute')" />
|
||||
<WorkflowRunButton
|
||||
v-else
|
||||
:id="workflow.id"
|
||||
data-description="route to workflow run button"
|
||||
variant="link"
|
||||
:title="
|
||||
!workflow.deleted
|
||||
? `<b>Rerun</b><br>${getWorkflowName()}`
|
||||
: 'This workflow has been deleted.'
|
||||
"
|
||||
:disabled="workflow.deleted"
|
||||
full
|
||||
:version="workflow.version" />
|
||||
</BButtonGroup>
|
||||
<ButtonSpinner
|
||||
v-if="!props.invocation"
|
||||
id="run-workflow"
|
||||
data-description="execute workflow button"
|
||||
:wait="runWaiting"
|
||||
:disabled="runDisabled"
|
||||
size="sm"
|
||||
title="Run Workflow"
|
||||
@onClick="emit('on-execute')" />
|
||||
<WorkflowRunButton
|
||||
v-else
|
||||
:id="workflow.id"
|
||||
data-description="route to workflow run button"
|
||||
variant="link"
|
||||
:title="
|
||||
!workflow.deleted
|
||||
? `<b>Rerun</b><br>${getWorkflowName()}`
|
||||
: 'This workflow has been deleted.'
|
||||
"
|
||||
:disabled="workflow.deleted"
|
||||
force
|
||||
full
|
||||
:version="workflow.version" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.success" class="donemessagelarge">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { mount, type Wrapper } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import { generateRandomString } from "./testUtils";
|
||||
@@ -11,22 +12,69 @@ const WORKFLOW_ID = generateRandomString();
|
||||
const WORKFLOW_VERSION = 1;
|
||||
const WORKFLOW_RUN_BUTTON_SELECTOR = `[data-workflow-run="${WORKFLOW_ID}"]`;
|
||||
|
||||
async function mountWorkflowRunButton(props?: { id: string; version: number; full?: boolean }) {
|
||||
function getPath() {
|
||||
return `/workflows/run?id=${WORKFLOW_ID}&version=${WORKFLOW_VERSION}`;
|
||||
}
|
||||
|
||||
async function mountWorkflowRunButton(
|
||||
props?: { id: string; version: number; full?: boolean; force?: boolean },
|
||||
currentPath?: string
|
||||
) {
|
||||
const mockRouter = {
|
||||
push: jest.fn(),
|
||||
afterEach: jest.fn(),
|
||||
currentRoute: {
|
||||
fullPath: currentPath || getPath(),
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mount(WorkflowRunButton as object, {
|
||||
propsData: { ...props },
|
||||
localVue,
|
||||
mocks: {
|
||||
$router: mockRouter,
|
||||
},
|
||||
});
|
||||
|
||||
return { wrapper };
|
||||
return { wrapper, mockRouter };
|
||||
}
|
||||
|
||||
async function clickButton(button: Wrapper<Vue>) {
|
||||
// Remove the href and target attributes to prevent navigation error
|
||||
// This is done because the `BButton` has a `:to` prop as well as an `@click` event
|
||||
button.element.setAttribute("href", "javascript:void(0)");
|
||||
button.element.setAttribute("target", "");
|
||||
|
||||
await button.trigger("click");
|
||||
await flushPromises();
|
||||
}
|
||||
|
||||
describe("WorkflowRunButton.vue", () => {
|
||||
it("should render button with icon and route", async () => {
|
||||
const { wrapper } = await mountWorkflowRunButton({ id: WORKFLOW_ID, version: WORKFLOW_VERSION });
|
||||
it("should render button with icon and route to it", async () => {
|
||||
const { wrapper, mockRouter } = await mountWorkflowRunButton({ id: WORKFLOW_ID, version: WORKFLOW_VERSION });
|
||||
|
||||
const button = wrapper.find(WORKFLOW_RUN_BUTTON_SELECTOR);
|
||||
expect(button.attributes("title")).toBe("Run workflow");
|
||||
expect(button.text()).toBe("");
|
||||
expect(button.attributes("href")).toBe(`/workflows/run?id=${WORKFLOW_ID}&version=${WORKFLOW_VERSION}`);
|
||||
expect(button.attributes("href")).toBe(getPath());
|
||||
|
||||
await clickButton(button);
|
||||
|
||||
// Check that router.push was called with the correct arguments
|
||||
expect(mockRouter.push).toHaveBeenCalledWith(getPath());
|
||||
});
|
||||
|
||||
it("should force route if on the same path", async () => {
|
||||
const { wrapper, mockRouter } = await mountWorkflowRunButton(
|
||||
{ id: WORKFLOW_ID, version: WORKFLOW_VERSION, force: true },
|
||||
getPath()
|
||||
);
|
||||
|
||||
const button = wrapper.find(WORKFLOW_RUN_BUTTON_SELECTOR);
|
||||
|
||||
await clickButton(button);
|
||||
|
||||
// Check that router.push was called with the correct arguments
|
||||
expect(mockRouter.push).toHaveBeenCalledWith(getPath(), { force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BButton } from "bootstrap-vue";
|
||||
import { computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router/composables";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -20,13 +21,22 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
variant: "primary",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const runPath = computed(
|
||||
() => `/workflows/run?id=${props.id}${props.version !== undefined ? `&version=${props.version}` : ""}`
|
||||
);
|
||||
|
||||
function forceRunPath() {
|
||||
if (props.force) {
|
||||
window.open(runPath.value);
|
||||
function routeToPath() {
|
||||
if (props.force && route.fullPath === runPath.value) {
|
||||
// vue-router 4 supports a native force push with clean URLs,
|
||||
// but we're using a __vkey__ bit as a workaround
|
||||
// Only conditionally force to keep urls clean most of the time.
|
||||
// @ts-ignore - monkeypatched router, drop with migration.
|
||||
router.push(runPath.value, { force: true });
|
||||
} else {
|
||||
router.push(runPath.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -42,7 +52,7 @@ function forceRunPath() {
|
||||
class="text-decoration-none"
|
||||
:disabled="disabled"
|
||||
:to="runPath"
|
||||
@click="forceRunPath">
|
||||
@click="routeToPath">
|
||||
<FontAwesomeIcon :icon="faPlay" fixed-width />
|
||||
|
||||
<span v-if="full" v-localize>Run</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="chartContainer" class="chart" :style="style"></div>
|
||||
<div ref="chartContainer" :style="style"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -9,15 +9,18 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
export interface VisSpec {
|
||||
spec: VisualizationSpec;
|
||||
width: string;
|
||||
fillWidth?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VisSpec>(), {
|
||||
width: "100%",
|
||||
fillWidth: true,
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
return { width: props.width };
|
||||
if (props.fillWidth) {
|
||||
return { width: "100%" };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const chartContainer = ref<HTMLDivElement | null>(null);
|
||||
@@ -49,8 +52,3 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart {
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -275,18 +275,9 @@ function itemToPieChartSpec(item: piechartData) {
|
||||
type: "nominal",
|
||||
legend: {
|
||||
type: "symbol",
|
||||
title: "", // should be item.category_title but it doesn't align right so just hide it
|
||||
direction: "vertical",
|
||||
titleAlign: "right",
|
||||
padding: 10,
|
||||
// rowPadding: 3,
|
||||
labelOffset: 40,
|
||||
// symbolOffset: 50,
|
||||
labelLimit: 120,
|
||||
// labelAlign: 'center',
|
||||
columnPadding: 5,
|
||||
// clipHeight: 20,
|
||||
titleOrient: "top",
|
||||
title: item.category_title,
|
||||
titleFontSize: 16,
|
||||
labelFontSize: 14,
|
||||
},
|
||||
},
|
||||
tooltip: [
|
||||
@@ -381,6 +372,10 @@ function getTimingInTitle(timing: string): string {
|
||||
const timingInTitles = computed(() => {
|
||||
return getTimingInTitle(timing.value);
|
||||
});
|
||||
|
||||
const groupByInTitles = computed(() => {
|
||||
return attributeToLabel[groupBy.value];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -393,22 +388,22 @@ const timingInTitles = computed(() => {
|
||||
<b-dropdown-item @click="timing = 'minutes'">{{ getTimingInTitle("minutes") }}</b-dropdown-item>
|
||||
<b-dropdown-item @click="timing = 'hours'">{{ getTimingInTitle("hours") }}</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
<b-dropdown right text="Group By: Tool">
|
||||
<b-dropdown right :text="'Group By: ' + groupByInTitles">
|
||||
<b-dropdown-item @click="groupBy = 'tool_id'">Tool</b-dropdown-item>
|
||||
<b-dropdown-item @click="groupBy = 'step_id'">Workflow Step</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</BButtonGroup>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol v-if="wallclockAggregate && wallclockAggregate.values">
|
||||
<BCol v-if="wallclockAggregate && wallclockAggregate.values" class="text-center">
|
||||
<h2 class="h-l truncate text-center">
|
||||
Aggregate
|
||||
<HelpText :for-title="true" uri="galaxy.jobs.metrics.walltime" text="Runtime Time" /> (in
|
||||
{{ timingInTitles }})
|
||||
</h2>
|
||||
<VegaWrapper :spec="itemToPieChartSpec(wallclockAggregate)" />
|
||||
<VegaWrapper :spec="itemToPieChartSpec(wallclockAggregate)" :fill-width="false" />
|
||||
</BCol>
|
||||
<BCol v-if="allocatedCoreTimeAggregate && allocatedCoreTimeAggregate.values">
|
||||
<BCol v-if="allocatedCoreTimeAggregate && allocatedCoreTimeAggregate.values" class="text-center">
|
||||
<h2 class="h-l truncate text-center">
|
||||
Aggregate
|
||||
<HelpText
|
||||
@@ -417,14 +412,14 @@ const timingInTitles = computed(() => {
|
||||
text="Allocated Core Time" />
|
||||
(in {{ timingInTitles }})
|
||||
</h2>
|
||||
<VegaWrapper :spec="itemToPieChartSpec(allocatedCoreTimeAggregate)" />
|
||||
<VegaWrapper :spec="itemToPieChartSpec(allocatedCoreTimeAggregate)" :fill-width="false" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow v-for="({ spec, item }, key) in metrics" :key="key">
|
||||
<BCol>
|
||||
<h2 class="h-l truncate text-center">
|
||||
<span v-if="item.helpTerm">
|
||||
<HelpText :for-title="true" :uri="item.helpTerm" :text="key" />
|
||||
<HelpText :for-title="true" :uri="item.helpTerm" :text="`${key}`" />
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ key }}
|
||||
|
||||
@@ -15,7 +15,6 @@ import InvocationMessage from "@/components/WorkflowInvocationState/InvocationMe
|
||||
interface Props {
|
||||
invocation: WorkflowInvocationElementView;
|
||||
invocationAndJobTerminal: boolean;
|
||||
invocationSchedulingTerminal: boolean;
|
||||
isFullPage?: boolean;
|
||||
isSubworkflow?: boolean;
|
||||
}
|
||||
@@ -62,7 +61,6 @@ const uniqueMessages = computed(() => {
|
||||
:invocation="invocation"
|
||||
:workflow="workflow"
|
||||
:is-terminal="invocationAndJobTerminal"
|
||||
:is-scheduled="invocationSchedulingTerminal"
|
||||
:is-full-page="isFullPage"
|
||||
:show-minimap="isFullPage" />
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useInvocationStore } from "@/stores/invocationStore";
|
||||
import { useWorkflowStore } from "@/stores/workflowStore";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import { cancelWorkflowScheduling } from "./services";
|
||||
import {
|
||||
errorCount as jobStatesSummaryErrorCount,
|
||||
isTerminal,
|
||||
@@ -53,6 +52,7 @@ const stepStatesInterval = ref<any>(undefined);
|
||||
const jobStatesInterval = ref<any>(undefined);
|
||||
const invocationLoaded = ref(false);
|
||||
const errorMessage = ref<string | null>(null);
|
||||
const cancellingInvocation = ref(false);
|
||||
|
||||
// after the report tab is first activated, no longer lazy-render it from then on
|
||||
const reportActive = ref(false);
|
||||
@@ -199,6 +199,16 @@ watch(
|
||||
|
||||
const storeId = computed(() => (invocation.value ? `invocation-${invocation.value.id}` : undefined));
|
||||
|
||||
watch(
|
||||
() => invocationSchedulingTerminal.value,
|
||||
async (newVal, oldVal) => {
|
||||
if (oldVal && !newVal) {
|
||||
// If the invocation was terminal and now is not, start polling again
|
||||
await pollStepStatesUntilTerminal();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(stepStatesInterval.value);
|
||||
clearTimeout(jobStatesInterval.value);
|
||||
@@ -219,11 +229,16 @@ async function pollJobStatesUntilTerminal() {
|
||||
function onError(e: any) {
|
||||
console.error(e);
|
||||
}
|
||||
function onCancel() {
|
||||
emit("invocation-cancelled");
|
||||
}
|
||||
function cancelWorkflowSchedulingLocal() {
|
||||
cancelWorkflowScheduling(props.invocationId).then(onCancel).catch(onError);
|
||||
async function onCancel() {
|
||||
try {
|
||||
cancellingInvocation.value = true;
|
||||
await invocationStore.cancelWorkflowScheduling(props.invocationId);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
} finally {
|
||||
emit("invocation-cancelled");
|
||||
cancellingInvocation.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -243,6 +258,7 @@ function cancelWorkflowSchedulingLocal() {
|
||||
size="sm"
|
||||
class="text-decoration-none"
|
||||
variant="link"
|
||||
:disabled="cancellingInvocation || invocationState == 'cancelling'"
|
||||
@click="onCancel">
|
||||
<FontAwesomeIcon :icon="faSquare" fixed-width />
|
||||
Cancel
|
||||
@@ -301,9 +317,7 @@ function cancelWorkflowSchedulingLocal() {
|
||||
:invocation="invocation"
|
||||
:is-full-page="props.isFullPage"
|
||||
:invocation-and-job-terminal="invocationAndJobTerminal"
|
||||
:invocation-scheduling-terminal="invocationSchedulingTerminal"
|
||||
:is-subworkflow="isSubworkflow"
|
||||
@invocation-cancelled="cancelWorkflowSchedulingLocal" />
|
||||
:is-subworkflow="isSubworkflow" />
|
||||
</BTab>
|
||||
<BTab v-if="!isSubworkflow" title="Steps" lazy>
|
||||
<WorkflowInvocationSteps
|
||||
@@ -334,7 +348,7 @@ function cancelWorkflowSchedulingLocal() {
|
||||
</template>
|
||||
<InvocationReport v-if="invocationStateSuccess" :invocation-id="invocation.id" />
|
||||
</BTab>
|
||||
<BTab title="Export" lazy>
|
||||
<BTab title="Export" title-item-class="invocation-export-tab" lazy>
|
||||
<div v-if="invocationAndJobTerminal">
|
||||
<WorkflowInvocationExportOptions :invocation-id="invocation.id" />
|
||||
</div>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { getRootFromIndexLink } from "onload";
|
||||
|
||||
const getUrl = (path) => getRootFromIndexLink() + path;
|
||||
|
||||
export function getInvocationJobsSummary(invocationId) {
|
||||
const url = getUrl(`api/invocations/${invocationId}/jobs_summary`);
|
||||
return axios.get(url);
|
||||
}
|
||||
|
||||
export function cancelWorkflowScheduling(invocationId) {
|
||||
const url = getUrl(`api/invocations/${invocationId}`);
|
||||
return axios.delete(url);
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export const statePlaceholders: Record<string, string> = {
|
||||
};
|
||||
|
||||
/** Only one job needs to be in one of these states for the graph step to be in that state */
|
||||
const SINGLE_INSTANCE_STATES = ["error", "running", "paused"];
|
||||
const SINGLE_INSTANCE_STATES = ["error", "running", "paused", "deleting"];
|
||||
/** All jobs need to be in one of these states for the graph step to be in that state */
|
||||
const ALL_INSTANCES_STATES = ["deleted", "skipped", "new", "queued"];
|
||||
|
||||
@@ -293,6 +293,9 @@ export function useInvocationGraph(
|
||||
function getStepStateFromJobStates(jobStates: string[]): GraphStep["state"] | undefined {
|
||||
for (const state of SINGLE_INSTANCE_STATES) {
|
||||
if (jobStates.includes(state)) {
|
||||
if (state === "deleting") {
|
||||
return "deleted";
|
||||
}
|
||||
return state as GraphStep["state"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,24 @@ export const useInvocationStore = defineStore("invocationStore", () => {
|
||||
return data;
|
||||
}
|
||||
|
||||
const { getItemById: getInvocationById, fetchItemById: fetchInvocationForId } =
|
||||
useKeyedCache<WorkflowInvocation>(fetchInvocationDetails);
|
||||
async function cancelWorkflowScheduling(invocationId: string) {
|
||||
const { data, error } = await GalaxyApi().DELETE("/api/invocations/{invocation_id}", {
|
||||
params: {
|
||||
path: { invocation_id: invocationId },
|
||||
},
|
||||
});
|
||||
if (error) {
|
||||
rethrowSimple(error);
|
||||
}
|
||||
storedInvocations.value[invocationId] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
const {
|
||||
getItemById: getInvocationById,
|
||||
fetchItemById: fetchInvocationForId,
|
||||
storedItems: storedInvocations,
|
||||
} = useKeyedCache<WorkflowInvocation>(fetchInvocationDetails);
|
||||
|
||||
const { getItemById: getInvocationJobsSummaryById, fetchItemById: fetchInvocationJobsSummaryForId } =
|
||||
useKeyedCache<InvocationJobsSummary>(fetchInvocationJobsSummary);
|
||||
@@ -58,6 +74,7 @@ export const useInvocationStore = defineStore("invocationStore", () => {
|
||||
fetchInvocationJobsSummaryForId,
|
||||
getInvocationStepById,
|
||||
fetchInvocationStepById,
|
||||
cancelWorkflowScheduling,
|
||||
graphStepsByStoreId,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -195,6 +195,17 @@ $ui-margin-horizontal-large: $margin-v * 2;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-form-header-underlay::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -$margin-h;
|
||||
left: -0.5rem;
|
||||
right: -0.5rem;
|
||||
height: 50px;
|
||||
background: linear-gradient($white 75%, change-color($white, $alpha: 0));
|
||||
}
|
||||
|
||||
// popovers
|
||||
.popover {
|
||||
max-width: 70%;
|
||||
|
||||
@@ -891,6 +891,12 @@ invocations:
|
||||
step_job_details: '[data-step="${order_index}"] .invocation-step-job-details'
|
||||
step_job_information: '[data-step="${order_index}"] .invocation-step-job-details .info_data_table'
|
||||
step_job_information_tool_id: '[data-step="${order_index}"] .invocation-step-job-details .info_data_table #galaxy-tool-id'
|
||||
export_tab: '.invocation-export-tab'
|
||||
export_output_format: '[data-invocation-export-type="${type}"] .card-body'
|
||||
export_destination: '[data-invocation-export-destination="${destination}"] .card-body'
|
||||
wizard_next_button: '.wizard-actions .go-next-btn'
|
||||
wizard_export_button: '.wizard-actions .go-next-btn.btn-primary'
|
||||
export_download_link: '.download-link'
|
||||
|
||||
tour:
|
||||
popover:
|
||||
|
||||
@@ -620,7 +620,7 @@ class WorkflowContentsManager(UsesAnnotations):
|
||||
import_options = ImportOptions()
|
||||
import_options.deduplicate_subworkflows = True
|
||||
as_dict = python_to_workflow(as_dict, galaxy_interface, workflow_directory=None, import_options=import_options)
|
||||
raw_description = RawWorkflowDescription(as_dict, path)
|
||||
raw_description = RawWorkflowDescription(as_dict)
|
||||
created_workflow = self.build_workflow_from_raw_description(trans, raw_description, WorkflowCreateOptions())
|
||||
return created_workflow.workflow
|
||||
|
||||
@@ -959,8 +959,9 @@ class WorkflowContentsManager(UsesAnnotations):
|
||||
return wf_dict
|
||||
|
||||
def _sync_stored_workflow(self, trans, stored_workflow):
|
||||
workflow_path = stored_workflow.from_path
|
||||
self.store_workflow_to_path(workflow_path, stored_workflow, stored_workflow.latest_workflow, trans=trans)
|
||||
if trans.user_is_admin:
|
||||
workflow_path = stored_workflow.from_path
|
||||
self.store_workflow_to_path(workflow_path, stored_workflow, stored_workflow.latest_workflow, trans=trans)
|
||||
|
||||
def store_workflow_artifacts(self, directory, filename_base, workflow, **kwd):
|
||||
modern_workflow_path = os.path.join(directory, f"{filename_base}.gxwf.yml")
|
||||
|
||||
@@ -2655,7 +2655,7 @@ class PostJobAction(Base, RepresentById):
|
||||
workflow_step_id: Mapped[Optional[int]] = mapped_column(ForeignKey("workflow_step.id"), index=True)
|
||||
action_type: Mapped[str] = mapped_column(String(255))
|
||||
output_name: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
action_arguments: Mapped[Optional[bytes]] = mapped_column(MutableJSONType)
|
||||
_action_arguments: Mapped[Optional[Dict[str, Any]]] = mapped_column("action_arguments", MutableJSONType)
|
||||
workflow_step: Mapped[Optional["WorkflowStep"]] = relationship(
|
||||
back_populates="post_job_actions",
|
||||
primaryjoin=(lambda: WorkflowStep.id == PostJobAction.workflow_step_id),
|
||||
@@ -2668,6 +2668,18 @@ class PostJobAction(Base, RepresentById):
|
||||
self.workflow_step = workflow_step
|
||||
ensure_object_added_to_session(self, object_in_session=workflow_step)
|
||||
|
||||
@property
|
||||
def action_arguments(self):
|
||||
if self.action_type in ("HideDatasetAction", "DeleteIntermediatesAction") and self._action_arguments is True:
|
||||
# Fix up broken workflows resulting from imports with gxformat2 <= 0.20.0
|
||||
return {}
|
||||
else:
|
||||
return self._action_arguments
|
||||
|
||||
@action_arguments.setter
|
||||
def action_arguments(self, value: Dict[str, Any]):
|
||||
self._action_arguments = value
|
||||
|
||||
|
||||
class PostJobActionAssociation(Base, RepresentById):
|
||||
__tablename__ = "post_job_action_association"
|
||||
@@ -7016,11 +7028,6 @@ class HistoryDatasetCollectionAssociation(
|
||||
primaryjoin=copied_from_history_dataset_collection_association_id == id,
|
||||
remote_side=[id],
|
||||
uselist=False,
|
||||
back_populates="copied_to_history_dataset_collection_association",
|
||||
)
|
||||
copied_to_history_dataset_collection_association = relationship(
|
||||
"HistoryDatasetCollectionAssociation",
|
||||
back_populates="copied_from_history_dataset_collection_association",
|
||||
)
|
||||
implicit_input_collections: Mapped[List["ImplicitlyCreatedDatasetCollectionInput"]] = relationship(
|
||||
primaryjoin=(
|
||||
|
||||
@@ -999,14 +999,16 @@ class ModelImportStore(metaclass=abc.ABCMeta):
|
||||
# sense.
|
||||
hdca_copied_from_sinks = object_import_tracker.hdca_copied_from_sinks
|
||||
if copied_from_object_key in object_import_tracker.hdcas_by_key:
|
||||
hdca.copied_from_history_dataset_collection_association = object_import_tracker.hdcas_by_key[
|
||||
copied_from_object_key
|
||||
]
|
||||
source_hdca = object_import_tracker.hdcas_by_key[copied_from_object_key]
|
||||
if source_hdca is not hdca:
|
||||
# We may not have the copied source, in which case the first included HDCA in the chain
|
||||
# acts as the source, so here we make sure we don't create a cycle.
|
||||
hdca.copied_from_history_dataset_collection_association = source_hdca
|
||||
else:
|
||||
if copied_from_object_key in hdca_copied_from_sinks:
|
||||
hdca.copied_from_history_dataset_collection_association = object_import_tracker.hdcas_by_key[
|
||||
hdca_copied_from_sinks[copied_from_object_key]
|
||||
]
|
||||
source_hdca = object_import_tracker.hdcas_by_key[hdca_copied_from_sinks[copied_from_object_key]]
|
||||
if source_hdca is not hdca:
|
||||
hdca.copied_from_history_dataset_collection_association = source_hdca
|
||||
else:
|
||||
hdca_copied_from_sinks[copied_from_object_key] = dataset_collection_key
|
||||
|
||||
@@ -1072,7 +1074,7 @@ class ModelImportStore(metaclass=abc.ABCMeta):
|
||||
for step_attrs in invocation_attrs["steps"]:
|
||||
imported_invocation_step = model.WorkflowInvocationStep()
|
||||
imported_invocation_step.workflow_invocation = imported_invocation
|
||||
ensure_object_added_to_session(imported_invocation, session=self.sa_session)
|
||||
ensure_object_added_to_session(imported_invocation_step, session=self.sa_session)
|
||||
attach_workflow_step(imported_invocation_step, step_attrs)
|
||||
restore_times(imported_invocation_step, step_attrs)
|
||||
imported_invocation_step.action = step_attrs["action"]
|
||||
@@ -1926,12 +1928,14 @@ class DirectoryModelExportStore(ModelExportStore):
|
||||
self.export_files = export_files
|
||||
self.included_datasets: Dict[model.DatasetInstance, Tuple[model.DatasetInstance, bool]] = {}
|
||||
self.dataset_implicit_conversions: Dict[model.DatasetInstance, model.ImplicitlyConvertedDatasetAssociation] = {}
|
||||
self.included_collections: List[Union[model.DatasetCollection, model.HistoryDatasetCollectionAssociation]] = []
|
||||
self.included_collections: Dict[
|
||||
Union[model.DatasetCollection, model.HistoryDatasetCollectionAssociation],
|
||||
Union[model.DatasetCollection, model.HistoryDatasetCollectionAssociation],
|
||||
] = {}
|
||||
self.included_libraries: List[model.Library] = []
|
||||
self.included_library_folders: List[model.LibraryFolder] = []
|
||||
self.included_invocations: List[model.WorkflowInvocation] = []
|
||||
self.collection_datasets: Set[int] = set()
|
||||
self.collections_attrs: List[Union[model.DatasetCollection, model.HistoryDatasetCollectionAssociation]] = []
|
||||
self.dataset_id_to_path: Dict[int, Tuple[Optional[str], Optional[str]]] = {}
|
||||
|
||||
self.job_output_dataset_associations: Dict[int, Dict[str, model.DatasetInstance]] = {}
|
||||
@@ -2298,8 +2302,7 @@ class DirectoryModelExportStore(ModelExportStore):
|
||||
def add_dataset_collection(
|
||||
self, collection: Union[model.DatasetCollection, model.HistoryDatasetCollectionAssociation]
|
||||
) -> None:
|
||||
self.collections_attrs.append(collection)
|
||||
self.included_collections.append(collection)
|
||||
self.included_collections[collection] = collection
|
||||
|
||||
def add_implicit_conversion_dataset(
|
||||
self,
|
||||
@@ -2362,7 +2365,7 @@ class DirectoryModelExportStore(ModelExportStore):
|
||||
|
||||
collections_attrs_filename = os.path.join(export_directory, ATTRS_FILENAME_COLLECTIONS)
|
||||
with open(collections_attrs_filename, "w") as collections_attrs_out:
|
||||
collections_attrs_out.write(to_json(self.collections_attrs))
|
||||
collections_attrs_out.write(to_json(self.included_collections.values()))
|
||||
|
||||
conversions_attrs_filename = os.path.join(export_directory, ATTRS_FILENAME_CONVERSIONS)
|
||||
with open(conversions_attrs_filename, "w") as conversions_attrs_out:
|
||||
@@ -2383,12 +2386,12 @@ class DirectoryModelExportStore(ModelExportStore):
|
||||
#
|
||||
|
||||
# Get all jobs associated with included HDAs.
|
||||
jobs_dict: Dict[str, model.Job] = {}
|
||||
jobs_dict: Dict[int, model.Job] = {}
|
||||
implicit_collection_jobs_dict = {}
|
||||
|
||||
def record_job(job):
|
||||
if not job:
|
||||
# No viable job.
|
||||
if not job or job.id in jobs_dict:
|
||||
# No viable job or job already recorded.
|
||||
return
|
||||
|
||||
jobs_dict[job.id] = job
|
||||
@@ -2414,10 +2417,11 @@ class DirectoryModelExportStore(ModelExportStore):
|
||||
)
|
||||
job_hda = hda
|
||||
while job_hda.copied_from_history_dataset_association: # should this check library datasets as well?
|
||||
# record job (if one exists) even if dataset was copied
|
||||
# copy could have been created manually through UI/API or using database operation tool,
|
||||
# in which case we have a relevant job to export.
|
||||
record_associated_jobs(job_hda)
|
||||
job_hda = job_hda.copied_from_history_dataset_association
|
||||
if not job_hda.creating_job_associations:
|
||||
# No viable HDA found.
|
||||
continue
|
||||
|
||||
record_associated_jobs(job_hda)
|
||||
|
||||
|
||||
@@ -738,7 +738,7 @@ class BuildMulledDockerContainerResolver(CliContainerResolver):
|
||||
"namespace": namespace,
|
||||
"hash_func": self.hash_func,
|
||||
"command": "build-and-test",
|
||||
"use_mamba": True,
|
||||
"use_mamba": False,
|
||||
}
|
||||
self._mulled_kwds["channels"] = default_mulled_conda_channels_from_env() or self._get_config_option(
|
||||
"mulled_channels", DEFAULT_CHANNELS
|
||||
@@ -789,7 +789,7 @@ class BuildMulledSingularityContainerResolver(SingularityCliContainerResolver):
|
||||
"namespace": "local",
|
||||
"singularity": True,
|
||||
"singularity_image_dir": self.cache_directory.path,
|
||||
"use_mamba": True,
|
||||
"use_mamba": False,
|
||||
}
|
||||
self.involucro_context = InvolucroContext(**self._involucro_context_kwds)
|
||||
auto_init = self._get_config_option("involucro_auto_init", True)
|
||||
|
||||
@@ -894,12 +894,14 @@ class InputsSelectOptionsDefinesOptions(Linter):
|
||||
# TODO check if input param is present for from_dataset
|
||||
from_dataset = options.get("from_dataset", None)
|
||||
from_data_table = options.get("from_data_table", None)
|
||||
from_url = options.get("from_url", None)
|
||||
|
||||
if (
|
||||
from_file is None
|
||||
and from_parameter is None
|
||||
and from_dataset is None
|
||||
and from_data_table is None
|
||||
and from_url is None
|
||||
and not filter_adds_options
|
||||
):
|
||||
lint_ctx.error(
|
||||
|
||||
@@ -3875,14 +3875,13 @@ class HarmonizeTool(DatabaseOperationTool):
|
||||
final_sorted_identifiers = [
|
||||
element.element_identifier for element in elements1 if element.element_identifier in old_elements2_dict
|
||||
]
|
||||
# Raise Exception if it is empty
|
||||
if len(final_sorted_identifiers) == 0:
|
||||
# Create empty collections:
|
||||
output_collections.create_collection(
|
||||
next(iter(self.outputs.values())), "output1", elements={}, propagate_hda_tags=False
|
||||
self.outputs["output1"], "output1", elements={}, propagate_hda_tags=False
|
||||
)
|
||||
output_collections.create_collection(
|
||||
next(iter(self.outputs.values())), "output2", elements={}, propagate_hda_tags=False
|
||||
self.outputs["output2"], "output2", elements={}, propagate_hda_tags=False
|
||||
)
|
||||
return
|
||||
|
||||
@@ -3900,7 +3899,7 @@ class HarmonizeTool(DatabaseOperationTool):
|
||||
self._add_datasets_to_history(history, new_elements.values())
|
||||
# Create collections:
|
||||
output_collections.create_collection(
|
||||
next(iter(self.outputs.values())), output_label, elements=new_elements, propagate_hda_tags=False
|
||||
self.outputs[output_label], output_label, elements=new_elements, propagate_hda_tags=False
|
||||
)
|
||||
|
||||
# Create outputs:
|
||||
|
||||
@@ -171,6 +171,37 @@
|
||||
<output_collection name="output1" type="list" count="0"/>
|
||||
<output_collection name="output2" type="list" count="0"/>
|
||||
</test>
|
||||
<!-- test that collection types get propagated to outputs correctly -->
|
||||
<test>
|
||||
<param name="input1">
|
||||
<collection type="list">
|
||||
<element name="element_1" value="simple_line.txt" />
|
||||
</collection>
|
||||
</param>
|
||||
<param name="input2">
|
||||
<collection type="list:paired">
|
||||
<element name="element_1">
|
||||
<collection type="paired">
|
||||
<element name="forward" value="1.fastqsanger" ftype="fastqsanger" />
|
||||
<element name="reverse" value="1.fastqsanger" ftype="fastqsanger" />
|
||||
</collection>
|
||||
</element>
|
||||
</collection>
|
||||
</param>
|
||||
<output_collection name="output1" type="list" count="1">
|
||||
<element name="element_1">
|
||||
<assert_contents>
|
||||
<has_text_matching expression="^This is a line of text.\n$" />
|
||||
</assert_contents>
|
||||
</element>
|
||||
</output_collection>
|
||||
<output_collection name="output2" type="list:paired" count="1">
|
||||
<element name="element_1">
|
||||
<element name="forward" file="1.fastqsanger" ftype="fastqsanger" />
|
||||
<element name="reverse" file="1.fastqsanger" ftype="fastqsanger" />
|
||||
</element>
|
||||
</output_collection>
|
||||
</test>
|
||||
</tests>
|
||||
<help><![CDATA[
|
||||
|
||||
|
||||
@@ -1911,6 +1911,8 @@ class BaseWorkflowPopulator(BasePopulator):
|
||||
invocations = self.history_invocations(history_id)
|
||||
if len(invocations) == expected_invocation_count:
|
||||
return True
|
||||
elif len(invocations) > expected_invocation_count:
|
||||
raise AssertionError("More than the expect number of invocations found in workflow")
|
||||
|
||||
wait_on(invocation_count, f"{expected_invocation_count} history invocations")
|
||||
for invocation in self.history_invocations(history_id):
|
||||
|
||||
@@ -32,6 +32,52 @@ from .framework import (
|
||||
class TestWorkflowRun(SeleniumTestCase, UsesHistoryItemAssertions, RunsWorkflows):
|
||||
ensure_registered = True
|
||||
|
||||
@selenium_test
|
||||
@managed_history
|
||||
def test_workflow_export_file_rocrate(self):
|
||||
self._setup_simple_invocation_for_export_testing()
|
||||
invocations = self.components.invocations
|
||||
invocations.export_tab.wait_for_and_click()
|
||||
self.screenshot("invocation_export_formats")
|
||||
invocations.export_output_format(type="ro-crate").wait_for_and_click()
|
||||
invocations.wizard_next_button.wait_for_and_click()
|
||||
download_option = invocations.export_destination(destination="download")
|
||||
download_option.wait_for_present()
|
||||
self.screenshot("invocation_export_rocrate_destinations")
|
||||
download_option.wait_for_and_click()
|
||||
invocations.wizard_next_button.wait_for_and_click()
|
||||
export_button = invocations.wizard_export_button
|
||||
export_button.wait_for_present()
|
||||
self.screenshot("invocation_export_rocrate_download_options")
|
||||
export_button.wait_for_and_click()
|
||||
self.sleep_for(self.wait_types.UX_TRANSITION)
|
||||
self.screenshot("invocation_export_crate_preparing_download")
|
||||
invocations.export_download_link.wait_for_present()
|
||||
self.screenshot("invocation_export_crate_download_ready")
|
||||
|
||||
@selenium_test
|
||||
@managed_history
|
||||
def test_workflow_export_file_native(self):
|
||||
self._setup_simple_invocation_for_export_testing()
|
||||
invocations = self.components.invocations
|
||||
invocations.export_tab.wait_for_and_click()
|
||||
self.screenshot("invocation_export_formats")
|
||||
invocations.export_output_format(type="default-file").wait_for_and_click()
|
||||
invocations.wizard_next_button.wait_for_and_click()
|
||||
download_option = invocations.export_destination(destination="download")
|
||||
download_option.wait_for_present()
|
||||
self.screenshot("invocation_export_native_destinations")
|
||||
download_option.wait_for_and_click()
|
||||
invocations.wizard_next_button.wait_for_and_click()
|
||||
export_button = invocations.wizard_export_button
|
||||
export_button.wait_for_present()
|
||||
self.screenshot("invocation_export_native_download_options")
|
||||
export_button.wait_for_and_click()
|
||||
self.sleep_for(self.wait_types.UX_TRANSITION)
|
||||
self.screenshot("invocation_export_native_preparing_download")
|
||||
invocations.export_download_link.wait_for_present()
|
||||
self.screenshot("invocation_export_native_download_ready")
|
||||
|
||||
@selenium_test
|
||||
@managed_history
|
||||
def test_simple_execution(self):
|
||||
@@ -376,3 +422,13 @@ steps: {}
|
||||
assert initial_value == "", initial_value
|
||||
input_element.clear()
|
||||
input_element.send_keys(value)
|
||||
|
||||
def _setup_simple_invocation_for_export_testing(self):
|
||||
# precondition: refresh history
|
||||
self.perform_upload(self.get_filename("1.fasta"))
|
||||
self.wait_for_history()
|
||||
self.workflow_run_open_workflow(WORKFLOW_SIMPLE_CAT_TWICE)
|
||||
self.workflow_run_submit()
|
||||
history_id = self.current_history_id()
|
||||
self.workflow_populator.wait_for_history_workflows(history_id, expected_invocation_count=1)
|
||||
return self.workflow_populator.history_invocations(history_id)[0]
|
||||
|
||||
@@ -17,6 +17,7 @@ from galaxy_test.api.test_workflows import RunsWorkflowFixtures
|
||||
from galaxy_test.base import api_asserts
|
||||
from galaxy_test.base.api import UsesCeleryTasks
|
||||
from galaxy_test.base.populators import (
|
||||
DatasetCollectionPopulator,
|
||||
DatasetPopulator,
|
||||
RunJobsSummary,
|
||||
WorkflowPopulator,
|
||||
@@ -27,6 +28,7 @@ from galaxy_test.driver.integration_util import IntegrationTestCase
|
||||
|
||||
class TestWorkflowTasksIntegration(PosixFileSourceSetup, IntegrationTestCase, UsesCeleryTasks, RunsWorkflowFixtures):
|
||||
dataset_populator: DatasetPopulator
|
||||
dataset_collection_populator: DatasetCollectionPopulator
|
||||
framework_tool_and_types = True
|
||||
|
||||
@classmethod
|
||||
@@ -37,6 +39,7 @@ class TestWorkflowTasksIntegration(PosixFileSourceSetup, IntegrationTestCase, Us
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.dataset_populator = DatasetPopulator(self.galaxy_interactor)
|
||||
self.dataset_collection_populator = DatasetCollectionPopulator(self.galaxy_interactor)
|
||||
self.workflow_populator = WorkflowPopulator(self.galaxy_interactor)
|
||||
self._write_file_fixtures()
|
||||
|
||||
@@ -124,6 +127,47 @@ class TestWorkflowTasksIntegration(PosixFileSourceSetup, IntegrationTestCase, Us
|
||||
invocation_details = self._export_and_import_workflow_invocation(summary, use_uris)
|
||||
self._rerun_imported_workflow(summary, invocation_details)
|
||||
|
||||
def test_export_import_invocation_with_copied_hdca_and_database_operation_tool(self):
|
||||
with self.dataset_populator.test_history() as history_id:
|
||||
self.dataset_collection_populator.create_list_in_history(history_id=history_id, wait=True).json()
|
||||
new_history = self.dataset_populator.copy_history(history_id=history_id).json()
|
||||
copied_collection = self.dataset_populator.get_history_collection_details(new_history["id"])
|
||||
workflow_id = self.workflow_populator.upload_yaml_workflow(
|
||||
"""class: GalaxyWorkflow
|
||||
inputs:
|
||||
input:
|
||||
type: collection
|
||||
collection_type: list
|
||||
steps:
|
||||
extract_dataset:
|
||||
tool_id: __EXTRACT_DATASET__
|
||||
in:
|
||||
input:
|
||||
source: input
|
||||
"""
|
||||
)
|
||||
inputs = {"input": {"src": "hdca", "id": copied_collection["id"]}}
|
||||
workflow_request = {"history": f"hist_id={new_history['id']}", "inputs_by": "name", "inputs": inputs}
|
||||
invocation = self.workflow_populator.invoke_workflow_raw(
|
||||
workflow_id, workflow_request, assert_ok=True
|
||||
).json()
|
||||
invocation_id = invocation["id"]
|
||||
self.workflow_populator.wait_for_invocation_and_jobs(history_id, workflow_id, invocation_id)
|
||||
jobs = self.workflow_populator.get_invocation_jobs(invocation_id)
|
||||
summary = RunJobsSummary(
|
||||
history_id=history_id,
|
||||
workflow_id=workflow_id,
|
||||
invocation_id=invocation["id"],
|
||||
inputs=inputs,
|
||||
jobs=jobs,
|
||||
invocation=invocation,
|
||||
workflow_request=workflow_request,
|
||||
)
|
||||
imported_invocation_details = self._export_and_import_workflow_invocation(summary)
|
||||
original_contents = self.dataset_populator.get_history_contents(new_history["id"])
|
||||
contents = self.dataset_populator.get_history_contents(imported_invocation_details["history_id"])
|
||||
assert len(contents) == len(original_contents) == 5
|
||||
|
||||
def _export_and_import_workflow_invocation(
|
||||
self, summary: RunJobsSummary, use_uris: bool = True, model_store_format="tgz"
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user