mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #11054 from jmchilton/history_export_ui
Revise user experience for history import and export.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<b-form-input v-model="localValue" :placeholder="placeholder" @click="selectFile"> </b-form-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BFormInput } from "bootstrap-vue";
|
||||
import { filesDialog } from "utils/data";
|
||||
|
||||
export default {
|
||||
components: { BFormInput },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: "file",
|
||||
},
|
||||
requireWritable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localValue: this.value,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
placeholder() {
|
||||
return `Click to select ${this.mode}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectFile() {
|
||||
const props = {
|
||||
mode: this.mode,
|
||||
requireWritable: this.requireWritable,
|
||||
};
|
||||
filesDialog((selected) => {
|
||||
this.localValue = selected?.url;
|
||||
}, props);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
localValue(newValue) {
|
||||
this.$emit("input", newValue);
|
||||
},
|
||||
value(newValue) {
|
||||
this.localValue = newValue;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -96,7 +96,7 @@
|
||||
key="export-history-to-file"
|
||||
title="Export History to File"
|
||||
icon="fas fa-file-archive"
|
||||
@click="iframeRedirect('/history/export_archive?preview=True')"
|
||||
@click="backboneRoute(`/histories/${history.id}/export`)"
|
||||
/>
|
||||
</PriorityMenu>
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<span>
|
||||
<b
|
||||
><a :href="link">{{ link }}</a></b
|
||||
>
|
||||
<font-awesome-icon
|
||||
v-b-tooltip.hover
|
||||
title="Copy export URL to your clipboard"
|
||||
icon="link"
|
||||
style="cursor: pointer;"
|
||||
@click="copyUrl"
|
||||
/>
|
||||
<i
|
||||
title="Information about when the history export was generated is included in the job details. Additionally, if there are issues with export, the job details may help figure out the underlying problem or communicate issues to your Galaxy administrator."
|
||||
>
|
||||
(<a href="#" @click="showDetails">view job details</a>)
|
||||
</i>
|
||||
<b-modal v-model="details" scrollable ok-only>
|
||||
<job-information :job_id="historyExport.job_id" :include-times="true" />
|
||||
</b-modal>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { BModal } from "bootstrap-vue";
|
||||
import { copy } from "utils/clipboard";
|
||||
import JobInformation from "components/JobInformation/JobInformation.vue";
|
||||
|
||||
library.add(faLink);
|
||||
|
||||
export default {
|
||||
components: { BModal, JobInformation, FontAwesomeIcon },
|
||||
props: {
|
||||
historyExport: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
details: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
link() {
|
||||
return this.historyExport.external_download_permanent_url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showDetails() {
|
||||
this.details = true;
|
||||
},
|
||||
copyUrl() {
|
||||
copy(this.latestExportUrl, "Export URL copied to your clipboard");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import Index from "./Index.vue";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
|
||||
const TEST_PLUGINS_URL = "/api/remote_files/plugins";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("Index.vue", () => {
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
axiosMock.onGet(TEST_PLUGINS_URL).reply(200, [{ id: "foo", writable: false }]);
|
||||
});
|
||||
|
||||
it("should render tabs", () => {
|
||||
// just make sure the component renders to catch obvious big errors
|
||||
const wrapper = shallowMount(Index, {
|
||||
propsData: {
|
||||
historyId: "test_id",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
expect(wrapper.exists("b-tabs-stub")).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<span>
|
||||
<h2>Export history archive</h2>
|
||||
<span v-if="initializing">
|
||||
<loading-span message="Loading server configuration." />
|
||||
</span>
|
||||
<span v-else-if="hasWritableFileSources">
|
||||
<b-card no-body>
|
||||
<b-tabs pills card vertical>
|
||||
<b-tab title="to a link" active>
|
||||
<b-card-text>
|
||||
<ToLink :history-id="historyId" />
|
||||
</b-card-text>
|
||||
</b-tab>
|
||||
<b-tab title="to a remote file">
|
||||
<ToRemoteFile :history-id="historyId" />
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</b-card>
|
||||
</span>
|
||||
<span v-else>
|
||||
<ToLink :history-id="historyId" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import ToLink from "./ToLink.vue";
|
||||
import ToRemoteFile from "./ToRemoteFile.vue";
|
||||
import { Services } from "components/FilesDialog/services";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpan,
|
||||
ToLink,
|
||||
ToRemoteFile,
|
||||
},
|
||||
props: {
|
||||
historyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
initializing: true,
|
||||
hasWritableFileSources: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.initialize();
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
const fileSources = await new Services().getFileSources();
|
||||
this.hasWritableFileSources = fileSources.some((fs) => fs.writable);
|
||||
console.log(fileSources);
|
||||
this.initializing = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import ToLink from "./ToLink.vue";
|
||||
import flushPromises from "flush-promises";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import axios from "axios";
|
||||
import { waitOnJob } from "components/JobStates/wait";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
const TEST_HISTORY_ID = "hist1235";
|
||||
const TEST_EXPORTS_URL = `/api/histories/${TEST_HISTORY_ID}/exports`;
|
||||
const TEST_JOB_ID = "test1234job";
|
||||
|
||||
jest.mock("components/JobStates/wait");
|
||||
|
||||
describe("ToLink.vue", () => {
|
||||
let axiosMock;
|
||||
let wrapper;
|
||||
|
||||
async function mountWithInitialExports(exports) {
|
||||
axiosMock.onGet(TEST_EXPORTS_URL).reply(200, exports);
|
||||
wrapper = shallowMount(ToLink, {
|
||||
propsData: {
|
||||
historyId: TEST_HISTORY_ID,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find("loading-span-stub").exists()).toBeTruthy();
|
||||
await flushPromises();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
it("should display a link if no exports ever generated", async () => {
|
||||
await mountWithInitialExports([]);
|
||||
expect(wrapper.find(".export-link")).toBeTruthy();
|
||||
expect(wrapper.find("loading-span-stub").exists()).toBeFalsy(); // loading span gone
|
||||
});
|
||||
|
||||
it("should start polling if latest export is preparing", async () => {
|
||||
let then = null;
|
||||
waitOnJob.mockReturnValue(
|
||||
new Promise((then_) => {
|
||||
then = then_;
|
||||
})
|
||||
);
|
||||
await mountWithInitialExports([
|
||||
{
|
||||
preparing: true,
|
||||
job_id: TEST_JOB_ID,
|
||||
},
|
||||
]);
|
||||
expect(then).toBeTruthy();
|
||||
expect(wrapper.vm.waitingOnJob).toBeTruthy();
|
||||
expect(wrapper.find("loading-span-stub").exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-alert show variant="danger" v-if="errorMessage" dismissible @dismissed="errorMessage = null">
|
||||
{{ errorMessage }}
|
||||
<JobError
|
||||
v-if="jobError"
|
||||
style="margin-top: 15px;"
|
||||
header="History export job ended in error"
|
||||
:job="jobError"
|
||||
/>
|
||||
</b-alert>
|
||||
<div v-if="loadingExports">
|
||||
<loading-span message="Loading history export information from Galaxy server." />
|
||||
</div>
|
||||
<div v-else-if="waitingOnJob">
|
||||
<loading-span message="Galaxy server is preparing history for download, this will likely take a while." />
|
||||
</div>
|
||||
<div v-else-if="latestExportReady">
|
||||
Link for download ready
|
||||
<export-link :historyExport="latestExport" />
|
||||
. Use this link to download the archive or import it on another Galaxy server.
|
||||
</div>
|
||||
<div v-else-if="hasReadyExport">
|
||||
<p>An out of date export is ready <export-link :historyExport="latestReadyExport" />.</p>
|
||||
|
||||
<p>
|
||||
The history has changed since this export was generated,
|
||||
<a class="export-link" href="#" @click.prevent="regenerateExport"
|
||||
>click here to generate a new archive for the current history state</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>No link for history export ready, {{ whyNoLink }}.</p>
|
||||
<p>
|
||||
<b
|
||||
><a class="export-link" href="#" @click.prevent="regenerateExport"
|
||||
>Click here to to generate a new archive for this history.</a
|
||||
></b
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { errorMessageAsString } from "utils/simple-error";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import ExportLink from "./ExportLink.vue";
|
||||
import { waitOnJob } from "components/JobStates/wait";
|
||||
import JobError from "components/JobInformation/JobError";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: { LoadingSpan, ExportLink, JobError },
|
||||
props: {
|
||||
historyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: null,
|
||||
exportsInitialized: false,
|
||||
exports: null,
|
||||
loadingExports: false,
|
||||
waitingOnJob: false,
|
||||
jobError: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (!this.exportsInitialized) {
|
||||
this.exportsInitialized = true;
|
||||
this.loadExports();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasExports() {
|
||||
return this.exports !== null && this.exports.length > 0;
|
||||
},
|
||||
latestExport() {
|
||||
return this.exports && this.exports[0];
|
||||
},
|
||||
readyExports() {
|
||||
return (this.exports || []).filter((e) => e.ready);
|
||||
},
|
||||
hasReadyExport() {
|
||||
return this.latestReadyExport !== null;
|
||||
},
|
||||
latestReadyExport() {
|
||||
return this.readyExports.length > 0 ? this.readyExports[0] : null;
|
||||
},
|
||||
latestExportReady() {
|
||||
return this.latestExport?.ready && this.latestExport?.up_to_date;
|
||||
},
|
||||
latestExportPreparing() {
|
||||
return this.latestExport?.preparing;
|
||||
},
|
||||
whyNoLink() {
|
||||
if (!this.hasExports) {
|
||||
return `no history export ever initiated for this history`;
|
||||
} else {
|
||||
return `previous history export archival likely failed`;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadExports() {
|
||||
this.loadingExports = true;
|
||||
const url = `${getAppRoot()}api/histories/${this.historyId}/exports`;
|
||||
axios
|
||||
.get(url)
|
||||
.then((response) => {
|
||||
this.loadingExports = false;
|
||||
this.exports = response.data;
|
||||
if (this.latestExportPreparing) {
|
||||
this.waitOnExportJob(this.latestExport.job_id);
|
||||
} else {
|
||||
this.waitingOnJob = false;
|
||||
}
|
||||
})
|
||||
.catch(this.handleError);
|
||||
},
|
||||
regenerateExport() {
|
||||
this.waitingOnJob = true;
|
||||
const url = `${getAppRoot()}api/histories/${this.historyId}/exports`;
|
||||
axios
|
||||
.put(url)
|
||||
.then((response) => {
|
||||
const status = response.status;
|
||||
if (status == 200) {
|
||||
this.loadExports();
|
||||
} else if (status == 202) {
|
||||
this.waitOnExportJob(response.data.job_id);
|
||||
} else {
|
||||
// error ....
|
||||
this.errorMessage = `Unexpected error while polling history export ${errorMessageAsString(
|
||||
response
|
||||
)}`;
|
||||
}
|
||||
})
|
||||
.catch(this.handleError);
|
||||
},
|
||||
waitOnExportJob(jobId) {
|
||||
this.waitingOnJob = true;
|
||||
this.jobError = false;
|
||||
waitOnJob(jobId)
|
||||
.then((jobResponse) => {
|
||||
this.waitingOnJob = false;
|
||||
this.loadingExports = true;
|
||||
// Race condition, for some reasons the job API returns
|
||||
// ok before the JEHA...
|
||||
setTimeout(this.loadExports, 2000);
|
||||
})
|
||||
.catch(this.handleError);
|
||||
},
|
||||
handleError(err) {
|
||||
this.waitingOnJob = false;
|
||||
this.loadingExports = false;
|
||||
this.errorMessage = errorMessageAsString(err, "Error generating a history export link");
|
||||
if (err?.data?.stderr) {
|
||||
this.jobError = err.data;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import ToRemoteFile from "./ToRemoteFile.vue";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import axios from "axios";
|
||||
import flushPromises from "flush-promises";
|
||||
import { waitOnJob } from "components/JobStates/wait";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
const TEST_HISTORY_ID = "hist1235";
|
||||
const TEST_JOB_ID = "job123789";
|
||||
const TEST_EXPORTS_URL = `/api/histories/${TEST_HISTORY_ID}/exports`;
|
||||
|
||||
jest.mock("components/JobStates/wait");
|
||||
|
||||
describe("ToRemoteFile.vue", () => {
|
||||
let axiosMock;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
wrapper = shallowMount(ToRemoteFile, {
|
||||
propsData: {
|
||||
historyId: TEST_HISTORY_ID,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
it("should render a form with export disable because inputs empty", async () => {
|
||||
expect(wrapper.find(".export-button").exists()).toBeTruthy();
|
||||
expect(wrapper.find(".export-button").attributes("disabled")).toBeTruthy();
|
||||
expect(wrapper.vm.canExport).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should allow export when name and directory available", async () => {
|
||||
await wrapper.setData({
|
||||
name: "export.tar.gz",
|
||||
directory: "gxfiles://",
|
||||
});
|
||||
expect(wrapper.vm.directory).toEqual("gxfiles://");
|
||||
expect(wrapper.vm.name).toEqual("export.tar.gz");
|
||||
expect(wrapper.vm.canExport).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should issue export PUT request on export", async () => {
|
||||
await wrapper.setData({
|
||||
name: "export.tar.gz",
|
||||
directory: "gxfiles://",
|
||||
});
|
||||
let request;
|
||||
axiosMock.onPut(TEST_EXPORTS_URL).reply((request_) => {
|
||||
request = request_;
|
||||
return [200, { job_id: TEST_JOB_ID }];
|
||||
});
|
||||
waitOnJob.mockReturnValue(
|
||||
new Promise((then) => {
|
||||
then({ state: "ok" });
|
||||
})
|
||||
);
|
||||
wrapper.vm.doExport();
|
||||
await flushPromises();
|
||||
const putData = JSON.parse(request.data);
|
||||
expect(putData.directory_uri).toEqual("gxfiles://");
|
||||
expect(putData.file_name).toEqual("export.tar.gz");
|
||||
expect(wrapper.find("b-alert-stub").attributes("variant")).toEqual("success");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-alert v-if="errorMessage" show variant="danger" dismissible @dismissed="errorMessage = null">
|
||||
{{ errorMessage }}
|
||||
<JobError
|
||||
v-if="jobError"
|
||||
style="margin-top: 15px;"
|
||||
header="History export job ended in error"
|
||||
:job="jobError"
|
||||
/>
|
||||
</b-alert>
|
||||
<div v-if="waitingOnJob">
|
||||
<loading-span message="Executing history export job, this will likely take a while." />
|
||||
</div>
|
||||
<div v-else-if="jobComplete">
|
||||
<b-alert show variant="success" dismissible @dismissed="reset">
|
||||
<h4>Done!</h4>
|
||||
<p>History successfully exported.</p>
|
||||
</b-alert>
|
||||
</div>
|
||||
<div v-else>
|
||||
<b-form-group
|
||||
id="directory"
|
||||
label-for="directory"
|
||||
description="Select a 'remote files' directory to export history archive to."
|
||||
class="mt-3"
|
||||
>
|
||||
<files-input v-model="directory" mode="directory" :requireWritable="true" />
|
||||
</b-form-group>
|
||||
<b-form-group id="name" label-for="name" description="Give the exported file a name." class="mt-3">
|
||||
<b-form-input id="name" v-model="name" placeholder="Name" required></b-form-input>
|
||||
</b-form-group>
|
||||
<b-row align-h="end">
|
||||
<b-col
|
||||
><b-button class="export-button" variant="primary" @click="doExport" :disabled="!canExport"
|
||||
>Export</b-button
|
||||
></b-col
|
||||
>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import { waitOnJob } from "components/JobStates/wait";
|
||||
import { errorMessageAsString } from "utils/simple-error";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import FilesInput from "components/FilesDialog/FilesInput.vue";
|
||||
import JobError from "components/JobInformation/JobError";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilesInput,
|
||||
LoadingSpan,
|
||||
JobError,
|
||||
},
|
||||
props: {
|
||||
historyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
canExport() {
|
||||
return !!this.name && !!this.directory;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: null,
|
||||
name: null,
|
||||
directory: null,
|
||||
waitingOnJob: false,
|
||||
jobComplete: false,
|
||||
jobError: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
doExport() {
|
||||
const data = {
|
||||
directory_uri: this.directory,
|
||||
file_name: this.name,
|
||||
};
|
||||
this.waitingOnJob = true;
|
||||
const url = `${getAppRoot()}api/histories/${this.historyId}/exports`;
|
||||
axios
|
||||
.put(url, data)
|
||||
.then((response) => {
|
||||
waitOnJob(response.data.job_id)
|
||||
.then((data) => {
|
||||
this.waitingOnJob = false;
|
||||
this.jobComplete = true;
|
||||
})
|
||||
.catch(this.handleError);
|
||||
})
|
||||
.catch(this.handleError);
|
||||
},
|
||||
reset() {
|
||||
this.jobComplete = false;
|
||||
this.errorMessage = null;
|
||||
this.name = "";
|
||||
this.directory = "";
|
||||
this.jobError = null;
|
||||
},
|
||||
handleError(err) {
|
||||
this.waitingOnJob = false;
|
||||
this.errorMessage = errorMessageAsString(err, "History export failed.");
|
||||
if (err?.data?.stderr) {
|
||||
this.jobError = err.data;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as HistoryExport } from "./Index.vue";
|
||||
@@ -0,0 +1,82 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import HistoryImport from "./HistoryImport.vue";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import axios from "axios";
|
||||
import flushPromises from "flush-promises";
|
||||
import { waitOnJob } from "components/JobStates/wait";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
const TEST_JOB_ID = "job123789";
|
||||
const TEST_HISTORY_URI = "/api/histories";
|
||||
const TEST_SOURCE_URL = "http://galaxy.example/import";
|
||||
const TEST_PLUGINS_URL = "/api/remote_files/plugins";
|
||||
|
||||
jest.mock("components/JobStates/wait");
|
||||
|
||||
describe("HistoryImport.vue", () => {
|
||||
let axiosMock;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
axiosMock.onGet(TEST_PLUGINS_URL).reply(200, [{ id: "foo", writable: false }]);
|
||||
wrapper = shallowMount(HistoryImport, {
|
||||
propsData: {},
|
||||
localVue,
|
||||
});
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("should render a form with submit disabled because inputs empty", async () => {
|
||||
expect(wrapper.find(".import-button").exists()).toBeTruthy();
|
||||
expect(wrapper.find(".import-button").attributes("disabled")).toBeTruthy();
|
||||
expect(wrapper.vm.importReady).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should allow import when URL available", async () => {
|
||||
await wrapper.setData({
|
||||
sourceURL: TEST_SOURCE_URL,
|
||||
});
|
||||
expect(wrapper.vm.importReady).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should require an URI if that is the import type", async () => {
|
||||
await wrapper.setData({
|
||||
sourceURL: TEST_SOURCE_URL,
|
||||
importType: "sourceRemoteFilesUri",
|
||||
});
|
||||
expect(wrapper.vm.importReady).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should post to create a new history and wait on job when submitted", async () => {
|
||||
await wrapper.setData({
|
||||
sourceURL: TEST_SOURCE_URL,
|
||||
});
|
||||
let formData;
|
||||
axiosMock.onPost(TEST_HISTORY_URI).reply((request) => {
|
||||
formData = request.data;
|
||||
return [200, { job_id: TEST_JOB_ID }];
|
||||
});
|
||||
let then;
|
||||
waitOnJob.mockReturnValue(
|
||||
new Promise((then_) => {
|
||||
then = then_;
|
||||
})
|
||||
);
|
||||
wrapper.vm.submit();
|
||||
await flushPromises();
|
||||
expect(formData.get("archive_source")).toBe(TEST_SOURCE_URL);
|
||||
expect(wrapper.vm.waitingOnJob).toBeTruthy();
|
||||
|
||||
// complete job and make sure waitingOnJob is false and complete is true
|
||||
then({ state: "ok" });
|
||||
await flushPromises();
|
||||
expect(wrapper.vm.waitingOnJob).toBeFalsy();
|
||||
expect(wrapper.vm.complete).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
});
|
||||
@@ -1,57 +1,172 @@
|
||||
<template>
|
||||
<b-form @submit="submit">
|
||||
<b-card title="Import a history from an archive">
|
||||
<b-alert :show="hasErrorMessage" variant="danger">{{ errorMessage }}</b-alert>
|
||||
<p>Please provide a Galaxy history export URL or a history file.</p>
|
||||
<b-form-group label="Archived History URL"> <b-form-input type="url" v-model="sourceURL" /> </b-form-group>
|
||||
<b-form-group label="Archived History File"> <b-form-file v-model="sourceFile" /> </b-form-group>
|
||||
<b-button type="submit">Import history</b-button>
|
||||
</b-card>
|
||||
</b-form>
|
||||
<b-card title="Import a history from an archive">
|
||||
<b-alert v-if="errorMessage" variant="danger" dismissible @dismissed="errorMessage = null" show>
|
||||
{{ errorMessage }}
|
||||
<JobError
|
||||
v-if="jobError"
|
||||
style="margin-top: 15px;"
|
||||
header="History import job ended in error"
|
||||
:job="jobError"
|
||||
/>
|
||||
</b-alert>
|
||||
<div v-if="initializing">
|
||||
<loading-span message="Loading server configuration." />
|
||||
</div>
|
||||
<div v-else-if="waitingOnJob">
|
||||
<LoadingSpan message="Waiting on history import job, this may take a while." />
|
||||
</div>
|
||||
<div v-else-if="complete">
|
||||
<b-alert :show="complete" variant="success" dismissible @dismissed="complete = false">
|
||||
<h4>Done!</h4>
|
||||
<p>History imported, check out <a :href="historyLink">your histories</a>.</p>
|
||||
</b-alert>
|
||||
</div>
|
||||
<div v-else>
|
||||
<b-form @submit.prevent="submit">
|
||||
<b-form-group label="How would you like to specify the history archive?" v-slot="{ ariaDescribedby }">
|
||||
<b-form-radio-group
|
||||
v-model="importType"
|
||||
:aria-describedby="ariaDescribedby"
|
||||
name="import-type"
|
||||
stacked
|
||||
>
|
||||
<b-form-radio value="externalUrl">
|
||||
Export URL from another Galaxy instance
|
||||
<font-awesome-icon icon="external-link-alt" />
|
||||
</b-form-radio>
|
||||
<b-form-radio value="upload">
|
||||
Upload local file from your computer
|
||||
<font-awesome-icon icon="upload" />
|
||||
</b-form-radio>
|
||||
<b-form-radio value="remoteFilesUri" v-if="hasFileSources">
|
||||
Select a remote file (e.g. Galaxy's FTP)
|
||||
<font-awesome-icon icon="folder-open" />
|
||||
</b-form-radio>
|
||||
</b-form-radio-group>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="importType == 'externalUrl'" label="Archived History URL">
|
||||
<b-form-input type="url" v-model="sourceURL" />
|
||||
</b-form-group>
|
||||
<b-form-group v-if="importType == 'upload'" label="Archived History File">
|
||||
<b-form-file v-model="sourceFile" />
|
||||
</b-form-group>
|
||||
<b-form-group v-show="importType == 'remoteFilesUri'" label="Remote File">
|
||||
<!-- using v-show so we can have a persistent ref and launch dialog on select -->
|
||||
<files-input v-model="sourceRemoteFilesUri" ref="filesInput" />
|
||||
</b-form-group>
|
||||
<b-button class="import-button" variant="primary" type="submit" :disabled="!importReady"
|
||||
>Import history</b-button
|
||||
>
|
||||
</b-form>
|
||||
</div>
|
||||
</b-card>
|
||||
</template>
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faFolderOpen, faUpload, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import FilesInput from "components/FilesDialog/FilesInput.vue";
|
||||
import { waitOnJob } from "components/JobStates/wait";
|
||||
import { errorMessageAsString } from "utils/simple-error";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import JobError from "components/JobInformation/JobError";
|
||||
import { Services } from "components/FilesDialog/services";
|
||||
|
||||
library.add(faFolderOpen);
|
||||
library.add(faUpload);
|
||||
library.add(faExternalLinkAlt);
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: { FilesInput, FontAwesomeIcon, JobError, LoadingSpan },
|
||||
data() {
|
||||
return {
|
||||
initializing: true,
|
||||
importType: "externalUrl",
|
||||
sourceFile: null,
|
||||
sourceURL: null,
|
||||
sourceRemoteFilesUri: null,
|
||||
errorMessage: null,
|
||||
waitingOnJob: false,
|
||||
complete: false,
|
||||
jobError: null,
|
||||
hasFileSources: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.initialize();
|
||||
},
|
||||
computed: {
|
||||
hasErrorMessage() {
|
||||
return this.errorMessage != null;
|
||||
importReady() {
|
||||
const importType = this.importType;
|
||||
if (importType == "externalUrl") {
|
||||
return !!this.sourceURL;
|
||||
} else if (importType == "upload") {
|
||||
return !!this.sourceFile;
|
||||
} else if (importType == "remoteFilesUri") {
|
||||
return !!this.sourceRemoteFilesUri;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
historyLink() {
|
||||
return `${getAppRoot()}histories/list`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
const fileSources = await new Services().getFileSources();
|
||||
this.hasFileSources = fileSources.length > 0;
|
||||
this.initializing = false;
|
||||
},
|
||||
submit: function (ev) {
|
||||
ev.preventDefault();
|
||||
if (!this.sourceFile && !this.sourceURL) {
|
||||
this.errorMessage = "You must provide a history archive URL or file.";
|
||||
} else {
|
||||
const formData = new FormData();
|
||||
formData.append("archive_file", this.sourceFile);
|
||||
const formData = new FormData();
|
||||
const importType = this.importType;
|
||||
if (importType == "externalUrl") {
|
||||
formData.append("archive_source", this.sourceURL);
|
||||
axios
|
||||
.post(`${getAppRoot()}api/histories`, formData)
|
||||
.then((response) => {
|
||||
window.location = `${getAppRoot()}histories/list?message=${
|
||||
response.data.message
|
||||
}&status=success`;
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error.response.data && error.response.data.err_msg;
|
||||
this.errorMessage = message || "Import failed for an unknown reason.";
|
||||
});
|
||||
} else if (importType == "upload") {
|
||||
formData.append("archive_file", this.sourceFile);
|
||||
formData.append("archive_source", "");
|
||||
} else if (importType == "remoteFilesUri") {
|
||||
formData.append("archive_source", this.sourceRemoteFilesUri);
|
||||
}
|
||||
axios
|
||||
.post(`${getAppRoot()}api/histories`, formData)
|
||||
.then((response) => {
|
||||
this.waitingOnJob = true;
|
||||
waitOnJob(response.data.id)
|
||||
.then((jobResponse) => {
|
||||
this.waitingOnJob = false;
|
||||
this.complete = true;
|
||||
})
|
||||
.catch(this.handleError);
|
||||
})
|
||||
.catch(this.handleError);
|
||||
},
|
||||
handleError: function (err) {
|
||||
this.waitingOnJob = false;
|
||||
this.errorMessage = errorMessageAsString(err, "History import failed.");
|
||||
if (err?.data?.stderr) {
|
||||
this.jobError = err.data;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
importType() {
|
||||
if (this.importType == "remoteFilesUri" && !this.sourceRemoteFilesUri) {
|
||||
this.$refs.filesInput.selectFile();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-wrapper {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<b-card border-variant="danger" :header="header">
|
||||
<b-card-text>
|
||||
<div @click="showInfo = true">
|
||||
<a href="#">See full job details <font-awesome-icon icon="info-circle" /></a>
|
||||
</div>
|
||||
<div class="error-wrapper" v-if="job.stderr" @click="toggleExpanded">
|
||||
Job Standard Error
|
||||
<pre :class="errorClasses">{{ job.stderr }}</pre>
|
||||
<b v-if="!expanded">(Click to expand job standard error)</b>
|
||||
</div>
|
||||
<!-- TODO: modal for reporting error. -->
|
||||
</b-card-text>
|
||||
<b-modal v-model="showInfo" scrollable ok-only>
|
||||
<job-information :job_id="job.id" :include-times="true" />
|
||||
</b-modal>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JobInformation from "./JobInformation.vue";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faInfoCircle);
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: { JobInformation, FontAwesomeIcon },
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
header: {
|
||||
type: String,
|
||||
default: "Job ended in error",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
showInfo: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
errorClasses() {
|
||||
const classes = ["code"];
|
||||
if (!this.expanded) {
|
||||
classes.push("preview");
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleExpanded() {
|
||||
this.expanded = !this.expanded;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-wrapper {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,18 @@
|
||||
<td>Galaxy Tool Version:</td>
|
||||
<td id="galaxy-tool-version">{{ job.tool_version }}</td>
|
||||
</tr>
|
||||
<tr v-if="job && includeTimes">
|
||||
<td>Created</td>
|
||||
<td id="created" v-if="job.create_time">
|
||||
<UtcDate :date="job.create_time" mode="pretty" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="job && includeTimes">
|
||||
<td>Updated</td>
|
||||
<td id="created" v-if="job.update_time">
|
||||
<UtcDate :date="job.update_time" mode="pretty" />
|
||||
</td>
|
||||
</tr>
|
||||
<code-row id="command-line" v-if="job" :codeLabel="'Command Line'" :codeItem="job.command_line" />
|
||||
<code-row id="stdout" v-if="job" :codeLabel="'Tool Standard Output'" :codeItem="job.tool_stdout" />
|
||||
<code-row id="stderr" v-if="job" :codeLabel="'Tool Standard Error'" :codeItem="job.tool_stderr" />
|
||||
@@ -46,17 +58,23 @@ import { mapCacheActions } from "vuex-cache";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import DecodedId from "../DecodedId.vue";
|
||||
import CodeRow from "./CodeRow.vue";
|
||||
import UtcDate from "components/UtcDate";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CodeRow,
|
||||
DecodedId,
|
||||
UtcDate,
|
||||
},
|
||||
props: {
|
||||
job_id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
includeTimes: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
this.fetchJob(this.job_id);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import JOB_STATES_MODEL from "mvc/history/job-states-model";
|
||||
import axios from "axios";
|
||||
|
||||
export function waitOnJob(jobId, onStateUpdate = null, interval = 1000) {
|
||||
// full=true to capture standard error on last iteration for building
|
||||
// error messages.
|
||||
const jobUrl = `${getAppRoot()}api/jobs/${jobId}?full=true`;
|
||||
const checkCondition = function (resolve, reject) {
|
||||
axios
|
||||
.get(jobUrl)
|
||||
.then((jobResponse) => {
|
||||
const state = jobResponse.data.state;
|
||||
if (onStateUpdate !== null) {
|
||||
onStateUpdate(state);
|
||||
}
|
||||
if (JOB_STATES_MODEL.NON_TERMINAL_STATES.indexOf(state) !== -1) {
|
||||
setTimeout(checkCondition, interval, resolve, reject);
|
||||
} else if (JOB_STATES_MODEL.ERROR_STATES.indexOf(state) !== -1) {
|
||||
reject(jobResponse);
|
||||
} else {
|
||||
resolve(jobResponse);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
};
|
||||
|
||||
return new Promise(checkCondition);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import InteractiveTools from "components/InteractiveTools/InteractiveTools.vue";
|
||||
import LibraryFolder from "components/LibraryFolder/LibraryFolder.vue";
|
||||
import WorkflowList from "components/Workflow/WorkflowList.vue";
|
||||
import HistoryImport from "components/HistoryImport.vue";
|
||||
import { HistoryExport } from "components/HistoryExport/index";
|
||||
import HistoryView from "components/HistoryView.vue";
|
||||
import WorkflowInvocationReport from "components/Workflow/InvocationReport.vue";
|
||||
import WorkflowRun from "components/Workflow/Run/WorkflowRun.vue";
|
||||
@@ -87,6 +88,7 @@ export const getAnalysisRouter = (Galaxy) =>
|
||||
"(/)histories(/)rename(/)": "show_histories_rename",
|
||||
"(/)histories(/)sharing(/)": "show_histories_sharing",
|
||||
"(/)histories(/)import(/)": "show_histories_import",
|
||||
"(/)histories(/)(:history_id)(/)export(/)": "show_history_export",
|
||||
"(/)histories(/)permissions(/)": "show_histories_permissions",
|
||||
"(/)histories/view": "show_history_view",
|
||||
"(/)histories/show_structure": "show_history_structure",
|
||||
@@ -251,6 +253,12 @@ export const getAnalysisRouter = (Galaxy) =>
|
||||
this._display_vue_helper(HistoryImport);
|
||||
},
|
||||
|
||||
show_history_export: function (history_id) {
|
||||
this._display_vue_helper(HistoryExport, {
|
||||
historyId: history_id,
|
||||
});
|
||||
},
|
||||
|
||||
show_tools_view: function () {
|
||||
this.page.toolPanel?.component.hide();
|
||||
this.page.panels.right.hide();
|
||||
|
||||
@@ -208,4 +208,10 @@ var JobStatesSummaryCollection = Backbone.Collection.extend({
|
||||
},
|
||||
});
|
||||
|
||||
export default { JobStatesSummary, JobStatesSummaryCollection, FETCH_STATE_ON_ADD, NON_TERMINAL_STATES, ERROR_STATES };
|
||||
export default {
|
||||
JobStatesSummary,
|
||||
JobStatesSummaryCollection,
|
||||
FETCH_STATE_ON_ADD,
|
||||
NON_TERMINAL_STATES,
|
||||
ERROR_STATES,
|
||||
};
|
||||
|
||||
@@ -148,8 +148,13 @@ var menu = [
|
||||
},
|
||||
{
|
||||
html: _l("Export History to File"),
|
||||
href: "history/export_archive?preview=True",
|
||||
anon: true,
|
||||
func: function () {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy && Galaxy.currHistoryPanel && Galaxy.router) {
|
||||
Galaxy.router.push(`/histories/${Galaxy.currHistoryPanel.model.id}/export`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
html: _l("Beta Features"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function errorMessageAsString(e) {
|
||||
let message = "Request failed.";
|
||||
export function errorMessageAsString(e, defaultMessage = "Request failed.") {
|
||||
let message = defaultMessage;
|
||||
if (e && e.response && e.response.data && e.response.data.err_msg) {
|
||||
message = e.response.data.err_msg;
|
||||
} else if (e && e.response) {
|
||||
|
||||
@@ -169,6 +169,53 @@ class HistoryManager(sharable.SharableModelManager, deletable.PurgableManagerMix
|
||||
return jobs
|
||||
|
||||
|
||||
class HistoryExportView:
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def get_exports(self, trans, history_id):
|
||||
history = self._history(trans, history_id)
|
||||
matching_exports = history.exports
|
||||
return [self.serialize(trans, history_id, e) for e in matching_exports]
|
||||
|
||||
def serialize(self, trans, history_id, jeha):
|
||||
rval = jeha.to_dict()
|
||||
encoded_jeha_id = trans.security.encode_id(jeha.id)
|
||||
api_url = self.app.url_for("history_archive_download", id=history_id, jeha_id=encoded_jeha_id)
|
||||
# this URL is less likely to be blocked by a proxy and require an API key, so export
|
||||
# older-style controller version for use with within the GUI and such.
|
||||
external_url = self.app.url_for(controller='history', action="export_archive", id=history_id, qualified=True)
|
||||
external_permanent_url = self.app.url_for(controller='history', action="export_archive", id=history_id, jeha_id=encoded_jeha_id, qualified=True)
|
||||
rval["download_url"] = api_url
|
||||
rval["external_download_latest_url"] = external_url
|
||||
rval["external_download_permanent_url"] = external_permanent_url
|
||||
rval = trans.security.encode_all_ids(rval)
|
||||
return rval
|
||||
|
||||
def get_ready_jeha(self, trans, history_id, jeha_id="latest"):
|
||||
history = self._history(trans, history_id)
|
||||
matching_exports = history.exports
|
||||
if jeha_id != "latest":
|
||||
decoded_jeha_id = trans.security.decode_id(jeha_id)
|
||||
matching_exports = [e for e in matching_exports if e.id == decoded_jeha_id]
|
||||
if len(matching_exports) == 0:
|
||||
raise glx_exceptions.ObjectNotFound("Failed to find target history export")
|
||||
|
||||
jeha = matching_exports[0]
|
||||
if not jeha.ready:
|
||||
raise glx_exceptions.MessageException("Export not available or not yet ready.")
|
||||
|
||||
return jeha
|
||||
|
||||
def _history(self, trans, history_id):
|
||||
if history_id is not None:
|
||||
history = self.app.history_manager.get_accessible(trans.security.decode_id(history_id), trans.user, current_history=trans.history)
|
||||
else:
|
||||
history = trans.history
|
||||
return history
|
||||
|
||||
|
||||
class HistorySerializer(sharable.SharableModelSerializer, deletable.PurgableSerializerMixin):
|
||||
"""
|
||||
Interface/service object for serializing histories into dictionaries.
|
||||
|
||||
@@ -1667,6 +1667,15 @@ class JobExportHistoryArchive(RepresentById):
|
||||
jeha.history_attrs_filename = history_attrs_filename
|
||||
return jeha
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'job_id': self.job.id,
|
||||
'ready': self.ready,
|
||||
'preparing': self.preparing,
|
||||
'up_to_date': self.up_to_date,
|
||||
}
|
||||
|
||||
|
||||
class JobImportHistoryArchive(RepresentById):
|
||||
def __init__(self, job=None, history=None, archive_dir=None):
|
||||
|
||||
@@ -445,7 +445,8 @@ class ImportsHistoryMixin:
|
||||
# Run job to do import.
|
||||
history_imp_tool = trans.app.toolbox.get_tool('__IMPORT_HISTORY__')
|
||||
incoming = {'__ARCHIVE_SOURCE__' : archive_source, '__ARCHIVE_TYPE__' : archive_type}
|
||||
history_imp_tool.execute(trans, incoming=incoming)
|
||||
job, _ = history_imp_tool.execute(trans, incoming=incoming)
|
||||
return job
|
||||
|
||||
|
||||
class UsesLibraryMixin:
|
||||
|
||||
@@ -32,7 +32,6 @@ from galaxy.web import (
|
||||
expose_api_anonymous,
|
||||
expose_api_anonymous_and_sessionless,
|
||||
expose_api_raw,
|
||||
url_for
|
||||
)
|
||||
from galaxy.webapps.base.controller import (
|
||||
BaseAPIController,
|
||||
@@ -52,6 +51,7 @@ class HistoriesController(BaseAPIController, ExportsHistoryMixin, ImportsHistory
|
||||
self.user_manager = users.UserManager(app)
|
||||
self.workflow_manager = workflows.WorkflowsManager(app)
|
||||
self.manager = histories.HistoryManager(app)
|
||||
self.history_export_view = histories.HistoryExportView(app)
|
||||
self.serializer = histories.HistorySerializer(app)
|
||||
self.deserializer = histories.HistoryDeserializer(app)
|
||||
self.filters = histories.HistoryFilters(app)
|
||||
@@ -352,8 +352,10 @@ class HistoriesController(BaseAPIController, ExportsHistoryMixin, ImportsHistory
|
||||
archive_type = "file"
|
||||
else:
|
||||
raise exceptions.MessageException("Please provide a url or file.")
|
||||
self.queue_history_import(trans, archive_type=archive_type, archive_source=archive_source)
|
||||
return {"message": "Importing history from source '%s'. This history will be visible when the import is complete." % archive_source}
|
||||
job = self.queue_history_import(trans, archive_type=archive_type, archive_source=archive_source)
|
||||
job_dict = job.to_dict()
|
||||
job_dict["message"] = "Importing history from source '%s'. This history will be visible when the import is complete." % archive_source
|
||||
return trans.security.encode_all_ids(job_dict)
|
||||
|
||||
new_history = None
|
||||
# if a history id was passed, copy that history
|
||||
@@ -471,7 +473,16 @@ class HistoriesController(BaseAPIController, ExportsHistoryMixin, ImportsHistory
|
||||
user=trans.user, trans=trans, **self._parse_serialization_params(kwd, 'detailed'))
|
||||
|
||||
@expose_api
|
||||
def archive_export(self, trans, id, **kwds):
|
||||
def index_exports(self, trans, id):
|
||||
"""
|
||||
index_exports(self, trans, id)
|
||||
* GET /api/histories/{id}/exports:
|
||||
Get history exports.
|
||||
"""
|
||||
return self.history_export_view.get_exports(trans, id)
|
||||
|
||||
@expose_api
|
||||
def archive_export(self, trans, id, payload=None, **kwds):
|
||||
"""
|
||||
PUT /api/histories/{id}/exports
|
||||
|
||||
@@ -484,6 +495,7 @@ class HistoriesController(BaseAPIController, ExportsHistoryMixin, ImportsHistory
|
||||
:rtype: dict
|
||||
:returns: object containing url to fetch export from.
|
||||
"""
|
||||
kwds.update(payload or {})
|
||||
# PUT instead of POST because multiple requests should just result
|
||||
# in one object being created.
|
||||
history = self.manager.get_accessible(self.decode_id(id), trans.user, current_history=trans.history)
|
||||
@@ -509,6 +521,8 @@ class HistoriesController(BaseAPIController, ExportsHistoryMixin, ImportsHistory
|
||||
directory_uri=directory_uri,
|
||||
file_name=file_name,
|
||||
)
|
||||
else:
|
||||
job = jeha.job
|
||||
|
||||
if exporting_to_uri:
|
||||
# we don't have a jeha, there will never be a download_url. Just let
|
||||
@@ -516,13 +530,18 @@ class HistoriesController(BaseAPIController, ExportsHistoryMixin, ImportsHistory
|
||||
# written.
|
||||
job_id = trans.security.encode_id(job.id)
|
||||
return dict(job_id=job_id)
|
||||
|
||||
if up_to_date and jeha.ready:
|
||||
jeha_id = trans.security.encode_id(jeha.id)
|
||||
return dict(download_url=url_for("history_archive_download", id=id, jeha_id=jeha_id))
|
||||
return self.history_export_view.serialize(trans, id, jeha)
|
||||
else:
|
||||
# Valid request, just resource is not ready yet.
|
||||
trans.response.status = "202 Accepted"
|
||||
return ''
|
||||
if jeha:
|
||||
return self.history_export_view.serialize(trans, id, jeha)
|
||||
else:
|
||||
assert job is not None, "logic error, don't have a jeha or a job"
|
||||
job_id = trans.security.encode_id(job.id)
|
||||
return dict(job_id=job_id)
|
||||
|
||||
@expose_api_raw
|
||||
def archive_download(self, trans, id, jeha_id, **kwds):
|
||||
@@ -535,19 +554,7 @@ class HistoriesController(BaseAPIController, ExportsHistoryMixin, ImportsHistory
|
||||
code (instead of 202) with a JSON dictionary containing a
|
||||
``download_url``.
|
||||
"""
|
||||
# Seems silly to put jeha_id in here, but want GET to be immuatable?
|
||||
# and this is being accomplished this way.
|
||||
history = self.manager.get_accessible(self.decode_id(id), trans.user, current_history=trans.history)
|
||||
matching_exports = [e for e in history.exports if trans.security.encode_id(e.id) == jeha_id]
|
||||
if not matching_exports:
|
||||
raise exceptions.ObjectNotFound()
|
||||
|
||||
jeha = matching_exports[0]
|
||||
if not jeha.ready:
|
||||
# User should not have been given this URL, PUT export should have
|
||||
# return a 202.
|
||||
raise exceptions.MessageException("Export not available or not yet ready.")
|
||||
|
||||
jeha = self.history_export_view.get_ready_jeha(trans, id, jeha_id)
|
||||
return self.serve_ready_history_export(trans, jeha)
|
||||
|
||||
@expose_api
|
||||
|
||||
@@ -146,6 +146,7 @@ def app_factory(global_conf, load_app_kwds=None, **kwargs):
|
||||
webapp.add_client_route('/histories/citations')
|
||||
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/list_published')
|
||||
webapp.add_client_route('/histories/list_shared')
|
||||
webapp.add_client_route('/histories/rename')
|
||||
@@ -496,6 +497,9 @@ def populate_api_routes(webapp, app):
|
||||
controller='page_revisions',
|
||||
parent_resources=dict(member_name='page', collection_name='pages'))
|
||||
|
||||
webapp.mapper.connect("history_exports",
|
||||
"/api/histories/{id}/exports", controller="histories",
|
||||
action="index_exports", conditions=dict(method=["GET"]))
|
||||
webapp.mapper.connect("history_archive_export",
|
||||
"/api/histories/{id}/exports", controller="histories",
|
||||
action="archive_export", conditions=dict(method=["PUT"]))
|
||||
|
||||
@@ -240,6 +240,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.history_manager = managers.histories.HistoryManager(app)
|
||||
self.history_export_view = managers.histories.HistoryExportView(app)
|
||||
self.history_serializer = managers.histories.HistorySerializer(self.app)
|
||||
|
||||
@web.expose
|
||||
@@ -1097,39 +1098,13 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt
|
||||
# TODO: used in display_base.mako
|
||||
|
||||
@web.expose
|
||||
def export_archive(self, trans, id=None, gzip=True, include_hidden=False, include_deleted=False, preview=False):
|
||||
def export_archive(self, trans, id=None, jeha_id="latest"):
|
||||
""" Export a history to an archive. """
|
||||
#
|
||||
# Get history to export.
|
||||
#
|
||||
if id:
|
||||
history = self.history_manager.get_accessible(self.decode_id(id), trans.user, current_history=trans.history)
|
||||
else:
|
||||
# Use current history.
|
||||
history = trans.history
|
||||
id = trans.security.encode_id(history.id)
|
||||
if not history:
|
||||
return trans.show_error_message("This history does not exist or you cannot export this history.")
|
||||
# If history has already been exported and it has not changed since export, stream it.
|
||||
jeha = history.latest_export
|
||||
if jeha and jeha.up_to_date:
|
||||
if jeha.ready:
|
||||
if preview:
|
||||
url = url_for(controller='history', action="export_archive", id=id, qualified=True)
|
||||
return trans.show_message("History Ready: '%(n)s'. Use this link to download "
|
||||
"the archive or import it to another Galaxy server: "
|
||||
"<a href='%(u)s'>%(u)s</a>" % ({'n': history.name, 'u': url}))
|
||||
else:
|
||||
return self.serve_ready_history_export(trans, jeha)
|
||||
elif jeha.preparing:
|
||||
return trans.show_message("Still exporting history %(n)s; please check back soon. Link: <a href='%(s)s'>%(s)s</a>"
|
||||
% ({'n': history.name, 's': url_for(controller='history', action="export_archive", id=id, qualified=True)}))
|
||||
self.queue_history_export(trans, history, gzip=gzip, include_hidden=include_hidden, include_deleted=include_deleted)
|
||||
url = url_for(controller='history', action="export_archive", id=id, qualified=True)
|
||||
return trans.show_message("Exporting History '%(n)s'. You will need to <a href='%(share)s' target='_top'>make this history 'accessible'</a> in order to import this to another galaxy sever. <br/>"
|
||||
"Use this link to download the archive or import it to another Galaxy server: "
|
||||
"<a href='%(u)s'>%(u)s</a>" % ({'share': url_for('/histories/sharing', id=id), 'n': history.name, 'u': url}))
|
||||
# TODO: used in this file and index.mako
|
||||
jeha = self.history_export_view.get_ready_jeha(trans, id, jeha_id)
|
||||
return self.serve_ready_history_export(trans, jeha)
|
||||
|
||||
@web.expose
|
||||
@web.json
|
||||
|
||||
Reference in New Issue
Block a user