Skip to content

Commit

Permalink
Add basic task based history export UI
Browse files Browse the repository at this point in the history
  • Loading branch information
davelopez committed Nov 4, 2022
1 parent 3825453 commit 452ef8b
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 1 deletion.
69 changes: 69 additions & 0 deletions client/src/components/Common/ExportRecordDetails.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup>
import { computed } from "vue";
import { BCard, BCardTitle } from "bootstrap-vue";
import UtcDate from "components/UtcDate";
import LoadingSpan from "components/LoadingSpan";
import { formatDistanceToNow, parseISO } from "date-fns";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { ExportRecordModel } from "./models/exportRecordModel";
const props = defineProps({
record: {
type: ExportRecordModel,
required: true,
},
objectType: {
type: String,
required: true,
},
});
const emit = defineEmits(["onReimport"]);
const title = computed(() => (props.record.isReady ? `Exported` : `Export started`));
const elapsedTime = computed(() => formatDistanceToNow(parseISO(`${props.record.date}Z`), { addSuffix: true }));
const statusMessage = computed(() => {
if (props.record.hasFailed) {
return `Something failed during this export. Please try again and if the problem persist contact your administrator.`;
}
if (props.record.isUpToDate) {
return `This export contains the latest changes of the ${props.objectType}.`;
}
return `This export is outdated and contains the changes of this ${props.objectType} from ${elapsedTime.value}.`;
});
const readyMessage = computed(() => `You can do the following actions with this ${props.objectType} export:`);
const preparingMessage = computed(
() => `Preparing export. This may take some time depending on the size of your ${props.objectType}`
);
function reimportObject() {
emit("onReimport", props.record);
}
</script>

<template>
<b-card>
<b-card-title>
<b>{{ title }}</b> <UtcDate :date="props.record.date" mode="elapsed" />
</b-card-title>
<span v-if="props.record.isPreparing">
<loading-span :message="preparingMessage" />
</span>
<div v-else>
<font-awesome-icon v-if="props.record.isUpToDate" icon="check-circle" class="text-success" />
<font-awesome-icon v-else-if="props.record.hasFailed" icon="exclamation-circle" class="text-danger" />
<font-awesome-icon v-else icon="exclamation-triangle" class="text-warning" />
<span>
{{ statusMessage }}
</span>
<div v-if="props.record.isReady">
<p class="mt-3">
{{ readyMessage }}
</p>
<b-button v-if="props.record.canReimport" variant="primary" @click="reimportObject">
Reimport
</b-button>
</div>
</div>
</b-card>
</template>
38 changes: 38 additions & 0 deletions client/src/components/Common/models/exportRecordModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export class ExportRecordModel {
constructor(data) {
this._data = data;
}

get isReady() {
return this._data.ready || false;
}

get isPreparing() {
return this._data.preparing || false;
}

get isUpToDate() {
return this._data.up_to_date || false;
}

get hasFailed() {
return !this.isReady && !this.isPreparing;
}

get date() {
return this._data.create_time;
}

get taskUUID() {
return this._data.task_uuid;
}

// import_uri doesn't work for downloads
get canReimport() {
return this._data?.export_metadata?.result_data?.import_uri !== undefined;
}

get importLink() {
return this._data?.export_metadata?.result_data?.import_uri;
}
}
111 changes: 111 additions & 0 deletions client/src/components/History/Export/HistoryExport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script setup>
import { computed, ref, onMounted, watch } from "vue";
import { BAlert, BCard, BButton, BTab, BTabs } from "bootstrap-vue";
import ExportRecordDetails from "components/Common/ExportRecordDetails.vue";
import ExportToFileSourceForm from "components/Common/ExportForm.vue";
import { HistoryExportServices } from "./services";
import { useTaskMonitor } from "composables/useTaskMonitor";
import { useFileSources } from "composables/fileSources";
const service = new HistoryExportServices();
const { isRunning: isExportTaskRunning, waitForTask } = useTaskMonitor();
const { hasWritable: hasWritableFileSources } = useFileSources();
const props = defineProps({
historyId: {
type: String,
required: true,
},
});
const isLoadingRecords = ref(true);
const latestExportRecord = ref(null);
const isLatestExportReady = ref(false);
const previousExportRecords = ref(null);
const availableRecordsMessage = computed(() =>
isLoadingRecords.value
? "Loading export records..."
: "This history has no export records yet. You can choose one of the export options above."
);
const errorMessage = ref(null);
onMounted(async () => {
updateExports();
});
watch(isExportTaskRunning, async () => {
console.debug("Updating latest export after task finished");
updateLatestExport();
});
async function updateLatestExport() {
isLoadingRecords.value = true;
const latestExport = await service.getLatestExportRecord(props.historyId);
latestExportRecord.value = latestExport;
isLatestExportReady.value = latestExport?.isReady;
if (latestExport?.isPreparing) {
isLatestExportReady.value = false;
waitForTask(latestExport.taskUUID, 3000);
}
isLoadingRecords.value = false;
}
async function updateExports() {
updateLatestExport();
service.getExportRecords(props.historyId, { offset: 1, limit: 10 }).then((records) => {
previousExportRecords.value = records;
});
}
async function exportToFileSource(exportDirectory, fileName) {
await service.exportToFileSource(props.historyId, exportDirectory, fileName);
updateExports();
}
async function generateDownloadLink() {
await service.generateDownloadLink();
updateExports();
}
function reimportHistoryFromRecord(record) {
return service.reimportHistoryFromRecord(record);
}
</script>
<template>
<span class="history-export-component">
<h1 class="h-lg">Export history {{ props.historyId }}</h1>
<b-card no-body>
<b-tabs pills card>
<b-tab title="to link" title-link-class="tab-export-to-link" active>
<p>
Here you can generate a temporal link to download your packaged history. When your download link
expires or your history changes, you can re-generate the link again.
</p>
<b-button variant="primary" @click="generateDownloadLink">Generate Download Link</b-button>
</b-tab>
<b-tab v-if="hasWritableFileSources" title="to remote file" title-link-class="tab-export-to-file">
<p>
If you need a `more permanent` way of storing your exported history you can export it directly
to one of the available remote file sources here.
</p>
<export-to-file-source-form what="history" @export="exportToFileSource" />
</b-tab>
</b-tabs>
</b-card>
<export-record-details
v-if="latestExportRecord"
:record="latestExportRecord"
object-type="history"
class="mt-3"
@onReimport="reimportHistoryFromRecord" />
<b-alert v-else-if="errorMessage" variant="danger" class="mt-3" show>
{{ errorMessage }}
</b-alert>
<b-alert v-else variant="info" class="mt-3" show>
{{ availableRecordsMessage }}
</b-alert>
</span>
</template>
63 changes: 63 additions & 0 deletions client/src/components/History/Export/services.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import axios from "axios";
import { rethrowSimple } from "utils/simple-error";
import { safePath } from "utils/redirect";
import { ExportRecordModel } from "components/Common/models/exportRecordModel";

export class HistoryExportServices {
/**
* Gets a list of export records for the given history.
* @param {String} historyId
* @returns {Promise<ExportRecordModel[]>}
*/
async getExportRecords(historyId, params = { offset: undefined, limit: undefined }) {
const url = `/api/histories/${historyId}/exports`;
try {
const response = await axios.get(safePath(url), {
headers: { Accept: "application/vnd.galaxy.task.export+json" },
params: params,
});
return response.data.map((item) => new ExportRecordModel(item));
} catch (e) {
rethrowSimple(e);
}
}

/**
* Gets the latest export record for the given history.
* @param {String} historyId
* @returns {Promise<ExportRecordModel|null>}
*/
async getLatestExportRecord(historyId) {
try {
const records = await this.getExportRecords(historyId, { limit: 1 });
return records.length ? records.at(0) : null;
} catch (e) {
rethrowSimple(e);
}
}

async exportToFileSource(
historyId,
exportDirectory,
fileName,
options = { exportFormat: "rocrate.zip", include_files: true, include_deleted: false, include_hidden: false }
) {
const exportDirectoryUri = `${exportDirectory}/${fileName}.${options.exportFormat}`;
const writeStoreParams = {
target_uri: exportDirectoryUri,
model_store_format: options.exportFormat,
include_files: options.include_files,
include_deleted: options.include_deleted,
include_hidden: options.include_hidden,
};
return axios.post(`/api/histories/${historyId}/write_store`, writeStoreParams);
}

async generateDownloadLink() {
console.debug("TODO: Generate link");
}

async reimportHistoryFromRecord(record) {
console.debug("TODO: Reimport from", record.importLink);
}
}
5 changes: 4 additions & 1 deletion client/src/entry/analysis/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { APIKey } from "components/User/APIKey";
import { CloudAuth } from "components/User/CloudAuth";
import { ExternalIdentities } from "components/User/ExternalIdentities";
import { HistoryExport } from "components/HistoryExport/index";
import HistoryExportTasks from "components/History/Export/HistoryExport";
import { StorageDashboardRouter } from "components/User/DiskUsage";

Vue.use(VueRouter);
Expand Down Expand Up @@ -229,7 +230,9 @@ export function getRouter(Galaxy) {
},
{
path: "histories/:historyId/export",
component: HistoryExport,
get component() {
return Galaxy.config.enable_celery_tasks ? HistoryExportTasks : HistoryExport;
},
props: true,
},
{
Expand Down

0 comments on commit 452ef8b

Please sign in to comment.