mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-11-03 21:34:30 +08:00
Merge pull request #20666 from davelopez/history_export_wizard
Unify History export UX using wizard
This commit is contained in:
@@ -1,24 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faClock,
|
||||
faDownload,
|
||||
faExclamationCircle,
|
||||
faExclamationTriangle,
|
||||
faFileImport,
|
||||
faHourglassEnd,
|
||||
faLink,
|
||||
faSpinner,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
import type { ColorVariant } from ".";
|
||||
import type { ExportRecord } from "./models/exportRecordModel";
|
||||
import type { ColorVariant } from "@/components/Common";
|
||||
import type { CardAction, CardBadge } from "@/components/Common/GCard.types";
|
||||
import type { ExportRecord } from "@/components/Common/models/exportRecordModel";
|
||||
|
||||
import GButton from "@/components/BaseComponents/GButton.vue";
|
||||
import GCard from "@/components/Common/GCard.vue";
|
||||
import Heading from "@/components/Common/Heading.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
library.add(faCheckCircle, faClock, faExclamationCircle, faExclamationTriangle, faLink);
|
||||
|
||||
interface Props {
|
||||
record: ExportRecord;
|
||||
@@ -39,22 +39,88 @@ const emit = defineEmits<{
|
||||
(e: "onCopyDownloadLink", record: ExportRecord): void;
|
||||
}>();
|
||||
|
||||
const title = computed(() => (props.record.isReady ? `Exported` : `Export started`));
|
||||
const preparingMessage = computed(
|
||||
() => `Preparing export. This may take some time depending on the size of your ${props.objectType}`
|
||||
);
|
||||
const primaryActions = computed<CardAction[]>(() => {
|
||||
const actions: CardAction[] = [];
|
||||
|
||||
async function reimportObject() {
|
||||
emit("onReimport", props.record);
|
||||
}
|
||||
if (props.record.canDownload) {
|
||||
actions.push({
|
||||
id: "copy-download-link",
|
||||
label: "Copy Link",
|
||||
icon: faLink,
|
||||
title: "Copy the download link to clipboard",
|
||||
variant: "outline-primary",
|
||||
handler: () => emit("onCopyDownloadLink", props.record),
|
||||
});
|
||||
actions.push({
|
||||
id: "download",
|
||||
label: "Download",
|
||||
icon: faDownload,
|
||||
title: "Download the result",
|
||||
variant: "primary",
|
||||
handler: () => emit("onDownload", props.record),
|
||||
});
|
||||
}
|
||||
|
||||
function downloadObject() {
|
||||
emit("onDownload", props.record);
|
||||
}
|
||||
if (props.record.canReimport) {
|
||||
actions.push({
|
||||
id: "reimport",
|
||||
label: "Reimport",
|
||||
icon: faFileImport,
|
||||
title: `Reimport the ${props.objectType} from this export record`,
|
||||
variant: "primary",
|
||||
handler: () => emit("onReimport", props.record),
|
||||
});
|
||||
}
|
||||
|
||||
function copyDownloadLink() {
|
||||
emit("onCopyDownloadLink", props.record);
|
||||
}
|
||||
return actions;
|
||||
});
|
||||
|
||||
const badges = computed<CardBadge[]>(() => {
|
||||
const badges: CardBadge[] = [];
|
||||
if (props.record.isPreparing) {
|
||||
badges.push({
|
||||
id: "in-progress",
|
||||
title: `${props.objectType} is being prepared for download`,
|
||||
label: "In Progress",
|
||||
variant: "info",
|
||||
});
|
||||
}
|
||||
if (props.record.canDownload) {
|
||||
badges.push({
|
||||
id: "ready-to-download",
|
||||
title: `${props.objectType} is ready to download`,
|
||||
label: "Ready to Download",
|
||||
variant: "success",
|
||||
});
|
||||
}
|
||||
if (props.record.hasFailed) {
|
||||
badges.push({
|
||||
id: "failed",
|
||||
title: `Failed to prepare ${props.objectType} for download`,
|
||||
label: "Failed",
|
||||
variant: "danger",
|
||||
});
|
||||
} else if (props.record.canExpire) {
|
||||
if (props.record.hasExpired) {
|
||||
badges.push({
|
||||
id: "expired",
|
||||
title: `The ${props.objectType} has expired and can no longer be downloaded. Please generate a new export.`,
|
||||
label: "Expired",
|
||||
variant: "warning",
|
||||
icon: faHourglassEnd,
|
||||
});
|
||||
} else {
|
||||
badges.push({
|
||||
id: "expires-soon",
|
||||
label: `Expires ${props.record.expirationElapsedTime}`,
|
||||
title: `The ${props.objectType} was exported ${props.record.elapsedTime} and this download link will expire in ${props.record.expirationElapsedTime}`,
|
||||
variant: "warning",
|
||||
icon: faHourglassEnd,
|
||||
});
|
||||
}
|
||||
}
|
||||
return badges;
|
||||
});
|
||||
|
||||
function onMessageDismissed() {
|
||||
emit("onActionMessageDismissed");
|
||||
@@ -62,95 +128,68 @@ function onMessageDismissed() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="export-record-details">
|
||||
<Heading size="sm">
|
||||
<b>{{ title }}</b> {{ props.record.elapsedTime }}
|
||||
</Heading>
|
||||
|
||||
<p v-if="!props.record.isPreparing">
|
||||
Format: <b class="record-archive-format">{{ props.record.modelStoreFormat }}</b>
|
||||
</p>
|
||||
|
||||
<span v-if="props.record.isPreparing">
|
||||
<LoadingSpan :message="preparingMessage" />
|
||||
</span>
|
||||
<div v-else>
|
||||
<div v-if="props.record.hasFailed">
|
||||
<GCard id="latest-export-record" class="export-record-details" :primary-actions="primaryActions" :badges="badges">
|
||||
<template v-slot:title>
|
||||
<Heading h3 size="sm">
|
||||
<span v-if="props.record.isPreparing">
|
||||
<FontAwesomeIcon :icon="faSpinner" spin class="mr-1" />
|
||||
Preparing {{ props.objectType }} for download...
|
||||
</span>
|
||||
<span v-else>
|
||||
This {{ props.objectType }} was exported <b>{{ props.record.elapsedTime }}</b>
|
||||
</span>
|
||||
</Heading>
|
||||
</template>
|
||||
<template v-slot:description>
|
||||
<span v-if="props.record.hasFailed">
|
||||
<FontAwesomeIcon
|
||||
:icon="faExclamationCircle"
|
||||
class="text-danger record-failed-icon"
|
||||
title="Export failed" />
|
||||
|
||||
<span>
|
||||
Something failed during this export. Please try again and if the problem persist contact your
|
||||
Something failed during the export process. Please try again and if the problem persist contact your
|
||||
administrator.
|
||||
</span>
|
||||
|
||||
<BAlert show variant="danger">{{ props.record.errorMessage }}</BAlert>
|
||||
</div>
|
||||
<div v-else-if="props.record.isUpToDate" title="Up to date">
|
||||
<BAlert :show="props.record.errorMessage" variant="danger">{{ props.record.errorMessage }}</BAlert>
|
||||
</span>
|
||||
<span v-if="props.record.isUpToDate" title="Up to date">
|
||||
<FontAwesomeIcon :icon="faCheckCircle" class="text-success record-up-to-date-icon" />
|
||||
<span> This export record contains the latest changes of the {{ props.objectType }}. </span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span>
|
||||
This export record contains the latest changes of the {{ props.objectType }} in
|
||||
<b class="record-archive-format">{{ props.record.modelStoreFormat }}</b> format.
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<FontAwesomeIcon :icon="faExclamationTriangle" class="text-warning record-outdated-icon" />
|
||||
<span>
|
||||
This export is outdated and contains the changes of this {{ props.objectType }} from
|
||||
{{ props.record.elapsedTime }}.
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<p v-if="props.record.canExpire" class="mt-3">
|
||||
<span v-if="props.record.canExpire" class="mt-3">
|
||||
<span v-if="props.record.hasExpired">
|
||||
<FontAwesomeIcon :icon="faClock" class="text-danger record-expired-icon" />
|
||||
This download link has expired.
|
||||
<FontAwesomeIcon :icon="faHourglassEnd" class="text-danger record-expired-icon" />
|
||||
This download link has expired, and the result is no longer available. You can generate a new export
|
||||
before downloading it again.
|
||||
</span>
|
||||
<span v-else>
|
||||
<FontAwesomeIcon :icon="faClock" class="text-warning record-expiration-warning-icon" />
|
||||
<FontAwesomeIcon :icon="faHourglassEnd" class="text-warning record-expiration-warning-icon" />
|
||||
This download link expires {{ props.record.expirationElapsedTime }}.
|
||||
</span>
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div v-if="props.record.isReady">
|
||||
<p class="mt-3">You can do the following actions with this {{ props.objectType }} export:</p>
|
||||
|
||||
<BAlert
|
||||
v-if="props.actionMessage !== undefined"
|
||||
:variant="props.actionMessageVariant"
|
||||
show
|
||||
fade
|
||||
dismissible
|
||||
@dismissed="onMessageDismissed">
|
||||
{{ props.actionMessage }}
|
||||
</BAlert>
|
||||
<div v-else class="actions">
|
||||
<GButton
|
||||
v-if="props.record.canDownload"
|
||||
class="record-download-btn"
|
||||
color="blue"
|
||||
@click="downloadObject">
|
||||
Download
|
||||
</GButton>
|
||||
|
||||
<GButton
|
||||
v-if="props.record.canDownload"
|
||||
title="Copy Download Link"
|
||||
size="small"
|
||||
color="blue"
|
||||
transparent
|
||||
@click.stop="copyDownloadLink">
|
||||
<FontAwesomeIcon :icon="faLink" />
|
||||
</GButton>
|
||||
|
||||
<GButton
|
||||
v-if="props.record.canReimport"
|
||||
class="record-reimport-btn"
|
||||
color="blue"
|
||||
@click="reimportObject">
|
||||
Reimport
|
||||
</GButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BAlert
|
||||
v-if="props.actionMessage !== undefined"
|
||||
:variant="props.actionMessageVariant"
|
||||
show
|
||||
fade
|
||||
dismissible
|
||||
@dismissed="onMessageDismissed">
|
||||
{{ props.actionMessage }}
|
||||
</BAlert>
|
||||
</template>
|
||||
</GCard>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faDownload,
|
||||
@@ -9,16 +8,13 @@ import {
|
||||
faSpinner,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BCard, BCollapse, BLink, BTable } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { BTable } from "bootstrap-vue";
|
||||
|
||||
import type { ExportRecord } from "./models/exportRecordModel";
|
||||
|
||||
import GButton from "@/components/BaseComponents/GButton.vue";
|
||||
import GButtonGroup from "@/components/BaseComponents/GButtonGroup.vue";
|
||||
|
||||
library.add(faCheckCircle, faDownload, faExclamationCircle, faFileImport, faLink, faSpinner);
|
||||
|
||||
interface Props {
|
||||
records: ExportRecord[];
|
||||
}
|
||||
@@ -39,10 +35,6 @@ const fields = [
|
||||
{ key: "actions", label: "Actions" },
|
||||
];
|
||||
|
||||
const isExpanded = ref(false);
|
||||
|
||||
const title = computed(() => (isExpanded.value ? `Hide old export records` : `Show old export records`));
|
||||
|
||||
async function reimportObject(record: ExportRecord) {
|
||||
emit("onReimport", record);
|
||||
}
|
||||
@@ -57,105 +49,87 @@ function copyDownloadLink(record: ExportRecord) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BLink
|
||||
:class="isExpanded ? null : 'collapsed'"
|
||||
:aria-expanded="isExpanded ? 'true' : 'false'"
|
||||
aria-controls="collapse-previous"
|
||||
@click="isExpanded = !isExpanded">
|
||||
{{ title }}
|
||||
</BLink>
|
||||
<BTable :items="props.records" :fields="fields">
|
||||
<template v-slot:cell(elapsedTime)="row">
|
||||
<span :title="row.item.date">{{ row.value }}</span>
|
||||
</template>
|
||||
|
||||
<BCollapse id="collapse-previous" v-model="isExpanded">
|
||||
<BCard>
|
||||
<BTable :items="props.records" :fields="fields">
|
||||
<template v-slot:cell(elapsedTime)="row">
|
||||
<span :title="row.item.date">{{ row.value }}</span>
|
||||
</template>
|
||||
<template v-slot:cell(format)="row">
|
||||
<span>{{ row.item.modelStoreFormat }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:cell(format)="row">
|
||||
<span>{{ row.item.modelStoreFormat }}</span>
|
||||
</template>
|
||||
<template v-slot:cell(expires)="row">
|
||||
<span v-if="row.item.hasExpired">Expired</span>
|
||||
|
||||
<template v-slot:cell(expires)="row">
|
||||
<span v-if="row.item.hasExpired">Expired</span>
|
||||
<span v-else-if="row.item.expirationDate" :title="row.item.expirationDate">
|
||||
{{ row.item.expirationElapsedTime }}
|
||||
</span>
|
||||
|
||||
<span v-else-if="row.item.expirationDate" :title="row.item.expirationDate">
|
||||
{{ row.item.expirationElapsedTime }}
|
||||
</span>
|
||||
<span v-else>No</span>
|
||||
</template>
|
||||
|
||||
<span v-else>No</span>
|
||||
</template>
|
||||
<template v-slot:cell(isUpToDate)="row">
|
||||
<FontAwesomeIcon
|
||||
v-if="row.item.isUpToDate"
|
||||
:icon="faCheckCircle"
|
||||
class="text-success"
|
||||
title="This export record contains the latest changes." />
|
||||
<FontAwesomeIcon
|
||||
v-else
|
||||
:icon="faExclamationCircle"
|
||||
class="text-danger"
|
||||
title="This export record is outdated. Please consider generating a new export if you need the latest changes." />
|
||||
</template>
|
||||
|
||||
<template v-slot:cell(isUpToDate)="row">
|
||||
<FontAwesomeIcon
|
||||
v-if="row.item.isUpToDate"
|
||||
:icon="faCheckCircle"
|
||||
class="text-success"
|
||||
title="This export record contains the latest changes." />
|
||||
<FontAwesomeIcon
|
||||
v-else
|
||||
:icon="faExclamationCircle"
|
||||
class="text-danger"
|
||||
title="This export record is outdated. Please consider generating a new export if you need the latest changes." />
|
||||
</template>
|
||||
<template v-slot:cell(isReady)="row">
|
||||
<FontAwesomeIcon
|
||||
v-if="row.item.isReady"
|
||||
:icon="faCheckCircle"
|
||||
class="text-success"
|
||||
title="Ready to download or import." />
|
||||
<FontAwesomeIcon
|
||||
v-else-if="row.item.isPreparing"
|
||||
:icon="faSpinner"
|
||||
spin
|
||||
class="text-info"
|
||||
title="Exporting in progress..." />
|
||||
<FontAwesomeIcon
|
||||
v-else-if="row.item.hasExpired"
|
||||
:icon="faExclamationCircle"
|
||||
class="text-danger"
|
||||
title="The export has expired." />
|
||||
<FontAwesomeIcon v-else :icon="faExclamationCircle" class="text-danger" title="The export failed." />
|
||||
</template>
|
||||
|
||||
<template v-slot:cell(isReady)="row">
|
||||
<FontAwesomeIcon
|
||||
v-if="row.item.isReady"
|
||||
:icon="faCheckCircle"
|
||||
class="text-success"
|
||||
title="Ready to download or import." />
|
||||
<FontAwesomeIcon
|
||||
v-else-if="row.item.isPreparing"
|
||||
:icon="faSpinner"
|
||||
spin
|
||||
class="text-info"
|
||||
title="Exporting in progress..." />
|
||||
<FontAwesomeIcon
|
||||
v-else-if="row.item.hasExpired"
|
||||
:icon="faExclamationCircle"
|
||||
class="text-danger"
|
||||
title="The export has expired." />
|
||||
<FontAwesomeIcon
|
||||
v-else
|
||||
:icon="faExclamationCircle"
|
||||
class="text-danger"
|
||||
title="The export failed." />
|
||||
</template>
|
||||
<template v-slot:cell(actions)="row">
|
||||
<GButtonGroup aria-label="Actions">
|
||||
<GButton
|
||||
tooltip
|
||||
tooltip-placement="bottom"
|
||||
:disabled="!row.item.canDownload"
|
||||
title="Download"
|
||||
@click="downloadObject(row.item)">
|
||||
<FontAwesomeIcon :icon="faDownload" />
|
||||
</GButton>
|
||||
|
||||
<template v-slot:cell(actions)="row">
|
||||
<GButtonGroup aria-label="Actions">
|
||||
<GButton
|
||||
tooltip
|
||||
tooltip-placement="bottom"
|
||||
:disabled="!row.item.canDownload"
|
||||
title="Download"
|
||||
@click="downloadObject(row.item)">
|
||||
<FontAwesomeIcon :icon="faDownload" />
|
||||
</GButton>
|
||||
<GButton
|
||||
v-if="row.item.canDownload"
|
||||
tooltip
|
||||
tooltip-placement="bottom"
|
||||
title="Copy Download Link"
|
||||
@click.stop="copyDownloadLink(row.item)">
|
||||
<FontAwesomeIcon :icon="faLink" />
|
||||
</GButton>
|
||||
|
||||
<GButton
|
||||
v-if="row.item.canDownload"
|
||||
tooltip
|
||||
tooltip-placement="bottom"
|
||||
title="Copy Download Link"
|
||||
@click.stop="copyDownloadLink(row.item)">
|
||||
<FontAwesomeIcon :icon="faLink" />
|
||||
</GButton>
|
||||
|
||||
<GButton
|
||||
tooltip
|
||||
tooltip-placement="bottom"
|
||||
:disabled="!row.item.canReimport"
|
||||
title="Reimport"
|
||||
@click="reimportObject(row.item)">
|
||||
<FontAwesomeIcon :icon="faFileImport" />
|
||||
</GButton>
|
||||
</GButtonGroup>
|
||||
</template>
|
||||
</BTable>
|
||||
</BCard>
|
||||
</BCollapse>
|
||||
</div>
|
||||
<GButton
|
||||
tooltip
|
||||
tooltip-placement="bottom"
|
||||
:disabled="!row.item.canReimport"
|
||||
title="Reimport"
|
||||
@click="reimportObject(row.item)">
|
||||
<FontAwesomeIcon :icon="faFileImport" />
|
||||
</GButton>
|
||||
</GButtonGroup>
|
||||
</template>
|
||||
</BTable>
|
||||
</template>
|
||||
|
||||
@@ -9,10 +9,10 @@ import type {
|
||||
} from "@/api";
|
||||
|
||||
export interface ExportParams {
|
||||
readonly modelStoreFormat: ModelStoreFormat;
|
||||
readonly includeFiles: boolean;
|
||||
readonly includeDeleted: boolean;
|
||||
readonly includeHidden: boolean;
|
||||
modelStoreFormat: ModelStoreFormat;
|
||||
includeFiles: boolean;
|
||||
includeDeleted: boolean;
|
||||
includeHidden: boolean;
|
||||
}
|
||||
|
||||
export interface ExportRecord {
|
||||
@@ -30,7 +30,7 @@ export interface ExportRecord {
|
||||
readonly isStsDownload: boolean;
|
||||
readonly canDownload: boolean;
|
||||
readonly modelStoreFormat: ModelStoreFormat;
|
||||
readonly exportParams?: ExportParams;
|
||||
readonly exportParams?: Readonly<ExportParams>;
|
||||
readonly duration?: number | null;
|
||||
readonly canExpire: boolean;
|
||||
readonly isPermanent: boolean;
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { BCard, BCollapse, BFormCheckbox, BFormGroup, BFormSelect, BLink } from "bootstrap-vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
|
||||
import { AVAILABLE_EXPORT_FORMATS } from "@/api/histories.export";
|
||||
import type { ExportParams } from "@/components/Common/models/exportRecordModel";
|
||||
|
||||
interface Props {
|
||||
exportParams: ExportParams;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits(["onValueChanged"]);
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const title = computed(() => (isExpanded.value ? `Hide advanced export options` : `Show advanced export options`));
|
||||
const localOptions = reactive({
|
||||
modelStoreFormat: props.exportParams.modelStoreFormat,
|
||||
includeFiles: props.exportParams.includeFiles,
|
||||
includeDeleted: props.exportParams.includeDeleted,
|
||||
includeHidden: props.exportParams.includeHidden,
|
||||
});
|
||||
|
||||
function onValueChanged() {
|
||||
emit("onValueChanged", localOptions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BLink
|
||||
id="toggle-options-link"
|
||||
:class="isExpanded ? null : 'collapsed'"
|
||||
:aria-expanded="isExpanded ? 'true' : 'false'"
|
||||
aria-controls="collapse-options"
|
||||
@click="isExpanded = !isExpanded">
|
||||
{{ title }}
|
||||
</BLink>
|
||||
|
||||
<BCollapse id="collapse-options" v-model="isExpanded">
|
||||
<BCard>
|
||||
<BFormGroup label="Export Format:" label-for="format">
|
||||
<BFormSelect
|
||||
id="format-selector"
|
||||
v-model="localOptions.modelStoreFormat"
|
||||
:options="AVAILABLE_EXPORT_FORMATS"
|
||||
value-field="id"
|
||||
text-field="name"
|
||||
@change="onValueChanged" />
|
||||
</BFormGroup>
|
||||
|
||||
<BFormGroup label="Dataset files included in the package:">
|
||||
<BFormCheckbox
|
||||
id="include-files-check"
|
||||
v-model="localOptions.includeFiles"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Active
|
||||
</BFormCheckbox>
|
||||
|
||||
<BFormCheckbox
|
||||
id="include-deleted-check"
|
||||
v-model="localOptions.includeDeleted"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Deleted (not purged)
|
||||
</BFormCheckbox>
|
||||
|
||||
<BFormCheckbox
|
||||
id="include-hidden-check"
|
||||
v-model="localOptions.includeHidden"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Hidden
|
||||
</BFormCheckbox>
|
||||
</BFormGroup>
|
||||
</BCard>
|
||||
</BCollapse>
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,7 +7,6 @@ import { setActivePinia } from "pinia";
|
||||
import type { HistorySummary } from "@/api";
|
||||
import { useServerMock } from "@/api/client/__mocks__";
|
||||
import { fetchHistoryExportRecords } from "@/api/histories.export";
|
||||
import type { BrowsableFilesSourcePlugin } from "@/api/remoteFiles";
|
||||
import {
|
||||
EXPIRED_STS_DOWNLOAD_RECORD,
|
||||
FILE_SOURCE_STORE_RECORD,
|
||||
@@ -39,24 +38,12 @@ const FAKE_HISTORY: HistorySummary = {
|
||||
url: FAKE_HISTORY_URL,
|
||||
};
|
||||
|
||||
const REMOTE_FILES_API_RESPONSE: BrowsableFilesSourcePlugin[] = [
|
||||
{
|
||||
id: "test-posix-source",
|
||||
type: "posix",
|
||||
label: "TestSource",
|
||||
doc: "For testing",
|
||||
writable: true,
|
||||
browsable: true,
|
||||
requires_roles: undefined,
|
||||
requires_groups: undefined,
|
||||
supports: {
|
||||
pagination: false,
|
||||
search: false,
|
||||
sorting: false,
|
||||
},
|
||||
uri_root: "gxfiles://test-posix-source",
|
||||
},
|
||||
];
|
||||
const selectors = {
|
||||
historyName: "#history-name",
|
||||
latestExportRecord: "#latest-export-record",
|
||||
showPreviousExportRecordsButton: "#show-old-records-button",
|
||||
fatalErrorAlert: "#fatal-error-alert",
|
||||
} as const;
|
||||
|
||||
async function mountHistoryExport() {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
@@ -81,10 +68,6 @@ describe("HistoryExport.vue", () => {
|
||||
if (historyId === FAKE_HISTORY_ID) {
|
||||
return response(200).json(FAKE_HISTORY);
|
||||
}
|
||||
}),
|
||||
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json([]);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -92,22 +75,16 @@ describe("HistoryExport.vue", () => {
|
||||
it("should render the history name", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#history-name").text()).toBe(FAKE_HISTORY.name);
|
||||
expect(wrapper.find(selectors.historyName).text()).toBe(FAKE_HISTORY.name);
|
||||
});
|
||||
|
||||
it("should render export options", async () => {
|
||||
it("should not display the latest export record if there is no export record", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#history-export-options").exists()).toBe(true);
|
||||
expect(wrapper.find(selectors.latestExportRecord).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should display a message indicating there are no exports where there are none", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#no-export-records-alert").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should render previous records when there is more than one record", async () => {
|
||||
it("should display the previous records button when there is more than one record", async () => {
|
||||
mockFetchExportRecords.mockResolvedValue([
|
||||
RECENT_STS_DOWNLOAD_RECORD,
|
||||
FILE_SOURCE_STORE_RECORD,
|
||||
@@ -115,79 +92,14 @@ describe("HistoryExport.vue", () => {
|
||||
]);
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#previous-export-records").exists()).toBe(true);
|
||||
expect(wrapper.find(selectors.showPreviousExportRecordsButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not render previous records when there is one or less records", async () => {
|
||||
it("should not display the previous records button when there is one or less records", async () => {
|
||||
mockFetchExportRecords.mockResolvedValue([RECENT_STS_DOWNLOAD_RECORD]);
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#previous-export-records").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should display file sources tab if there are available", async () => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => response(200).json(REMOTE_FILES_API_RESPONSE))
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#direct-download-tab").exists()).toBe(true);
|
||||
expect(wrapper.find("#file-source-tab").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not display file sources tab if there are no file sources available", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#direct-download-tab").exists()).toBe(true);
|
||||
expect(wrapper.find("#file-source-tab").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should display the ZENODO tab if the Zenodo plugin is available", async () => {
|
||||
const zenodoPlugin: BrowsableFilesSourcePlugin = {
|
||||
id: "zenodo",
|
||||
type: "rdm",
|
||||
label: "Zenodo",
|
||||
doc: "For testing",
|
||||
writable: true,
|
||||
browsable: true,
|
||||
supports: {
|
||||
pagination: false,
|
||||
search: false,
|
||||
sorting: false,
|
||||
},
|
||||
uri_root: "zenodo://",
|
||||
};
|
||||
server.use(http.get("/api/remote_files/plugins", ({ response }) => response(200).json([zenodoPlugin])));
|
||||
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#zenodo-file-source-tab").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should display the ZENODO tab if the user has a user-defined Zenodo file source", async () => {
|
||||
const userZenodoPlugin: BrowsableFilesSourcePlugin = {
|
||||
id: "998c5bba-b18f-4223-9c93-0f36fa2fdae8",
|
||||
type: "zenodo",
|
||||
label: "ZENODO",
|
||||
doc: "My integration with Zenodo",
|
||||
browsable: true,
|
||||
writable: true,
|
||||
requires_roles: null,
|
||||
requires_groups: null,
|
||||
url: "https://zenodo.org/",
|
||||
supports: {
|
||||
pagination: true,
|
||||
search: true,
|
||||
sorting: false,
|
||||
},
|
||||
uri_root: "gxuserfiles://998c5bba-b18f-4223-9c93-0f36fa2fdae8",
|
||||
};
|
||||
server.use(http.get("/api/remote_files/plugins", ({ response }) => response(200).json([userZenodoPlugin])));
|
||||
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#zenodo-file-source-tab").exists()).toBe(true);
|
||||
expect(wrapper.find(selectors.showPreviousExportRecordsButton).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not display a fatal error alert if the history is found and loaded", async () => {
|
||||
@@ -195,11 +107,8 @@ describe("HistoryExport.vue", () => {
|
||||
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#fatal-error-alert").exists()).toBe(false);
|
||||
|
||||
expect(wrapper.find("#history-name").exists()).toBe(true);
|
||||
expect(wrapper.find("#history-export-options").exists()).toBe(true);
|
||||
expect(wrapper.find("#direct-download-tab").exists()).toBe(true);
|
||||
expect(wrapper.find(selectors.fatalErrorAlert).exists()).toBe(false);
|
||||
expect(wrapper.find(selectors.historyName).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not render the UI and display a fatal error message if the history cannot be found or loaded", async () => {
|
||||
@@ -217,10 +126,7 @@ describe("HistoryExport.vue", () => {
|
||||
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#fatal-error-alert").exists()).toBe(true);
|
||||
|
||||
expect(wrapper.find("#history-name").exists()).toBe(false);
|
||||
expect(wrapper.find("#history-export-options").exists()).toBe(false);
|
||||
expect(wrapper.find("#direct-download-tab").exists()).toBe(false);
|
||||
expect(wrapper.find(selectors.fatalErrorAlert).exists()).toBe(true);
|
||||
expect(wrapper.find(selectors.historyName).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faFileExport } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFileExport, faList } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BAlert, BButton, BCard, BTab, BTabs } from "bootstrap-vue";
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
import type { AnyHistory } from "@/api";
|
||||
@@ -12,24 +11,24 @@ import {
|
||||
reimportHistoryFromRecord,
|
||||
} from "@/api/histories.export";
|
||||
import type { ColorVariant } from "@/components/Common";
|
||||
import { areEqual, type ExportParams, type ExportRecord } from "@/components/Common/models/exportRecordModel";
|
||||
import type { ExportParams, ExportRecord } from "@/components/Common/models/exportRecordModel";
|
||||
import { useConfirmDialog } from "@/composables/confirmDialog";
|
||||
import { useDownloadTracker } from "@/composables/downloadTracker";
|
||||
import { useFileSources } from "@/composables/fileSources";
|
||||
import { DEFAULT_EXPORT_PARAMS, useShortTermStorage } from "@/composables/shortTermStorage";
|
||||
import { useShortTermStorage } from "@/composables/shortTermStorage";
|
||||
import { useTaskMonitor } from "@/composables/taskMonitor";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { copy as sendToClipboard } from "@/utils/clipboard";
|
||||
import { absPath } from "@/utils/redirect";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import ExportOptions from "./ExportOptions.vue";
|
||||
import ExportToFileSourceForm from "@/components/Common/ExportForm.vue";
|
||||
import ExportToRDMRepositoryForm from "@/components/Common/ExportRDMForm.vue";
|
||||
import type { HistoryExportData } from "./types";
|
||||
|
||||
import HistoryExportWizard from "./HistoryExportWizard.vue";
|
||||
import GButton from "@/components/BaseComponents/GButton.vue";
|
||||
import GModal from "@/components/BaseComponents/GModal.vue";
|
||||
import ExportRecordDetails from "@/components/Common/ExportRecordDetails.vue";
|
||||
import ExportRecordTable from "@/components/Common/ExportRecordTable.vue";
|
||||
import RDMCredentialsInfo from "@/components/Common/RDMCredentialsInfo.vue";
|
||||
import ExternalLink from "@/components/ExternalLink.vue";
|
||||
import Heading from "@/components/Common/Heading.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
const {
|
||||
@@ -40,14 +39,6 @@ const {
|
||||
hasFailed: taskHasFailed,
|
||||
} = useTaskMonitor();
|
||||
|
||||
const { hasWritable: hasWritableFileSources } = useFileSources({ exclude: ["rdm"] });
|
||||
const {
|
||||
hasWritable: hasWritableRDMFileSources,
|
||||
getFileSourceById,
|
||||
getFileSourcesByType,
|
||||
isPrivateFileSource,
|
||||
} = useFileSources({ include: ["rdm"] });
|
||||
|
||||
const {
|
||||
isPreparing: isPreparingDownload,
|
||||
prepareHistoryDownload,
|
||||
@@ -66,34 +57,18 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
library.add(faFileExport);
|
||||
|
||||
const POLLING_DELAY = 3000;
|
||||
|
||||
const exportParams = ref(DEFAULT_EXPORT_PARAMS);
|
||||
const isLoadingRecords = ref(true);
|
||||
const exportRecords = ref<ExportRecord[]>([]);
|
||||
const history = ref<AnyHistory>();
|
||||
const isLoadingHistory = ref(true);
|
||||
|
||||
const historyName = computed(() => history.value?.name ?? props.historyId);
|
||||
const defaultFileName = computed(() => `(Galaxy History) ${historyName.value}`);
|
||||
const latestExportRecord = computed(() => (exportRecords.value?.length ? exportRecords.value.at(0) : null));
|
||||
const isLatestExportRecordReadyToDownload = computed(
|
||||
() =>
|
||||
latestExportRecord.value &&
|
||||
latestExportRecord.value.isUpToDate &&
|
||||
latestExportRecord.value.canDownload &&
|
||||
areEqual(latestExportRecord.value.exportParams, exportParams.value)
|
||||
);
|
||||
const canGenerateDownload = computed(() => !isPreparingDownload.value && !isLatestExportRecordReadyToDownload.value);
|
||||
const previousExportRecords = computed(() => (exportRecords.value ? exportRecords.value.slice(1) : []));
|
||||
const hasPreviousExports = computed(() => previousExportRecords.value?.length > 0);
|
||||
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 isBusy = computed(() => isLoadingRecords.value || isPreparingDownload.value || isExportTaskRunning.value);
|
||||
|
||||
const historyStore = useHistoryStore();
|
||||
|
||||
@@ -101,7 +76,7 @@ const isFatalError = ref(false);
|
||||
const errorMessage = ref<string>();
|
||||
const actionMessage = ref<string>();
|
||||
const actionMessageVariant = ref<ColorVariant>();
|
||||
const zenodoSource = computed(() => getZenodoSource());
|
||||
const showOldRecords = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
if (await loadHistory()) {
|
||||
@@ -116,12 +91,6 @@ watch(isExportTaskRunning, (newValue, oldValue) => {
|
||||
}
|
||||
});
|
||||
|
||||
function getZenodoSource() {
|
||||
const zenodoSources = getFileSourcesByType("zenodo");
|
||||
// Prioritize the one provided by the user in case there are multiple
|
||||
return zenodoSources.find((fs) => isPrivateFileSource(fs) && fs.writable) ?? getFileSourceById("zenodo");
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
isLoadingHistory.value = true;
|
||||
try {
|
||||
@@ -164,15 +133,28 @@ async function updateExports() {
|
||||
}
|
||||
}
|
||||
|
||||
async function doExportToFileSource(exportDirectory: string, fileName: string) {
|
||||
await exportHistoryToFileSource(props.historyId, exportDirectory, fileName, exportParams.value);
|
||||
async function onExportHistory(exportData: HistoryExportData) {
|
||||
switch (exportData.destination) {
|
||||
case "download":
|
||||
await prepareDownload(exportData);
|
||||
break;
|
||||
case "remote-source":
|
||||
case "rdm-repository":
|
||||
case "zenodo-repository":
|
||||
await exportToFileSource(exportData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function exportToFileSource(exportData: HistoryExportData) {
|
||||
await exportHistoryToFileSource(props.historyId, exportData.remoteUri, exportData.outputFileName, exportData);
|
||||
updateExports();
|
||||
}
|
||||
|
||||
async function prepareDownload() {
|
||||
async function prepareDownload(exportParams: ExportParams) {
|
||||
const result = await prepareHistoryDownload(props.historyId, {
|
||||
pollDelayInMs: POLLING_DELAY,
|
||||
exportParams: exportParams.value,
|
||||
exportParams: exportParams,
|
||||
});
|
||||
if (result) {
|
||||
downloadTracker.trackDownloadRequestWithData({
|
||||
@@ -231,10 +213,8 @@ function onActionMessageDismissedFromRecord() {
|
||||
actionMessageVariant.value = undefined;
|
||||
}
|
||||
|
||||
function updateExportParams(newParams: ExportParams) {
|
||||
exportParams.value = {
|
||||
...newParams,
|
||||
};
|
||||
function onShowOldRecords() {
|
||||
showOldRecords.value = true;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -243,164 +223,72 @@ onUnmounted(() => {
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<span class="history-export-component">
|
||||
<FontAwesomeIcon icon="file-export" size="2x" class="text-primary float-left mr-2" />
|
||||
<h1 class="h-lg">
|
||||
<div class="history-export-component">
|
||||
<Heading h1 separator inline>
|
||||
<FontAwesomeIcon :icon="faFileExport" class="text-primary float-left mr-2" />
|
||||
Export
|
||||
<b v-if="isFatalError" class="text-danger">Error</b>
|
||||
<LoadingSpan v-else-if="!history" spinner-only />
|
||||
<b v-else id="history-name">{{ historyName }}</b>
|
||||
</h1>
|
||||
</Heading>
|
||||
|
||||
<BAlert v-if="isFatalError" id="fatal-error-alert" variant="danger" class="mt-3" show>
|
||||
{{ errorMessage }}
|
||||
</BAlert>
|
||||
|
||||
<div v-if="history">
|
||||
<ExportOptions
|
||||
id="history-export-options"
|
||||
class="mt-3"
|
||||
:export-params="exportParams"
|
||||
@onValueChanged="updateExportParams" />
|
||||
<div v-if="latestExportRecord">
|
||||
<Heading h2 size="md">Latest Export Record</Heading>
|
||||
<ExportRecordDetails
|
||||
v-if="latestExportRecord"
|
||||
:record="latestExportRecord"
|
||||
:action-message="actionMessage"
|
||||
:action-message-variant="actionMessageVariant"
|
||||
object-type="history"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onCopyDownloadLink="copyDownloadLinkFromRecord"
|
||||
@onReimport="reimportFromRecord"
|
||||
@onActionMessageDismissed="onActionMessageDismissedFromRecord" />
|
||||
|
||||
<h2 class="h-md mt-3">How do you want to export this history?</h2>
|
||||
<BCard no-body class="mt-3">
|
||||
<BTabs pills card vertical>
|
||||
<BTab
|
||||
id="direct-download-tab"
|
||||
title="to direct download"
|
||||
title-link-class="tab-export-to-link"
|
||||
active>
|
||||
<p>
|
||||
Here you can generate a temporal download for your history. When your download link expires
|
||||
or your history changes you can re-generate it again.
|
||||
</p>
|
||||
<GButton
|
||||
v-if="hasPreviousExports"
|
||||
id="show-old-records-button"
|
||||
outline
|
||||
color="blue"
|
||||
@click="onShowOldRecords">
|
||||
<FontAwesomeIcon :icon="faList" class="mr-1" />
|
||||
Show old export records
|
||||
</GButton>
|
||||
|
||||
<BAlert show variant="warning">
|
||||
History archive downloads can expire and are removed at regular intervals. For permanent
|
||||
storage, export to a <b>repository</b> or download and then import the archive on another
|
||||
Galaxy server.
|
||||
</BAlert>
|
||||
<Heading h2 size="md" class="mt-3">Export your history again</Heading>
|
||||
</div>
|
||||
|
||||
<BButton
|
||||
class="gen-direct-download-btn"
|
||||
:disabled="!canGenerateDownload"
|
||||
variant="primary"
|
||||
@click="prepareDownload">
|
||||
Generate direct download
|
||||
</BButton>
|
||||
|
||||
<span v-if="isPreparingDownload">
|
||||
<LoadingSpan message="Galaxy is preparing your download, this will likely take a while" />
|
||||
</span>
|
||||
<BAlert v-else-if="isLatestExportRecordReadyToDownload" variant="success" class="mt-3" show>
|
||||
The latest export record is ready. Use the download button below to download it or change
|
||||
the advanced export options above to generate a new one.
|
||||
</BAlert>
|
||||
</BTab>
|
||||
<BTab
|
||||
v-if="hasWritableFileSources"
|
||||
id="file-source-tab"
|
||||
title="to repository"
|
||||
title-link-class="tab-export-to-file">
|
||||
<p>
|
||||
If you need a "more permanent" way of storing your history archive you can export it
|
||||
directly to one of the available repositories. You will be able to re-import it later as
|
||||
long as it remains available on the remote server.
|
||||
</p>
|
||||
|
||||
<ExportToFileSourceForm
|
||||
what="history"
|
||||
:clear-input-after-export="true"
|
||||
@export="doExportToFileSource" />
|
||||
</BTab>
|
||||
<BTab
|
||||
v-if="hasWritableRDMFileSources"
|
||||
id="rdm-file-source-tab"
|
||||
title="to RDM repository"
|
||||
title-link-class="tab-export-to-rdm-repo">
|
||||
<p>You can <b>upload your history</b> to one of the available RDM repositories here.</p>
|
||||
|
||||
<RDMCredentialsInfo what="history export archive" />
|
||||
|
||||
<ExportToRDMRepositoryForm
|
||||
what="history"
|
||||
:default-filename="defaultFileName"
|
||||
:default-record-name="historyName"
|
||||
:clear-input-after-export="true"
|
||||
@export="doExportToFileSource" />
|
||||
</BTab>
|
||||
<BTab
|
||||
v-if="zenodoSource"
|
||||
id="zenodo-file-source-tab"
|
||||
:title="`to ${zenodoSource.label}`"
|
||||
title-link-class="tab-export-to-zenodo-repo">
|
||||
<div class="zenodo-info">
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/zenodo/zenodo/master/zenodo/modules/theme/static/img/logos/zenodo-gradient-square.svg"
|
||||
alt="ZENODO Logo" />
|
||||
<p>
|
||||
<ExternalLink href="https://zenodo.org"><b>Zenodo</b></ExternalLink> is a
|
||||
general-purpose open repository developed under the
|
||||
<ExternalLink href="https://www.openaire.eu">European OpenAIRE</ExternalLink> program
|
||||
and operated by <ExternalLink href="https://home.cern">CERN</ExternalLink>. It allows
|
||||
researchers to deposit research papers, data sets, research software, reports, and any
|
||||
other research related digital artefacts. For each submission, a persistent
|
||||
<b>digital object identifier (DOI)</b> is minted, which makes the stored items easily
|
||||
citeable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RDMCredentialsInfo
|
||||
what="history export archive"
|
||||
:selected-repository="zenodoSource"
|
||||
:is-private-file-source="isPrivateFileSource(zenodoSource)" />
|
||||
<ExportToRDMRepositoryForm
|
||||
what="history"
|
||||
:default-filename="defaultFileName"
|
||||
:default-record-name="historyName"
|
||||
:clear-input-after-export="true"
|
||||
:file-source="zenodoSource"
|
||||
@export="doExportToFileSource">
|
||||
</ExportToRDMRepositoryForm>
|
||||
</BTab>
|
||||
</BTabs>
|
||||
</BCard>
|
||||
<HistoryExportWizard
|
||||
:history-id="props.historyId"
|
||||
:history-name="history.name"
|
||||
:is-busy="isBusy"
|
||||
@onExport="onExportHistory" />
|
||||
|
||||
<BAlert v-if="errorMessage" id="last-export-record-error-alert" variant="danger" class="mt-3" show>
|
||||
{{ errorMessage }}
|
||||
</BAlert>
|
||||
<div v-else-if="latestExportRecord">
|
||||
<h2 class="h-md mt-3">Export Records</h2>
|
||||
<BCard>
|
||||
<ExportRecordDetails
|
||||
:record="latestExportRecord"
|
||||
object-type="history"
|
||||
:action-message="actionMessage"
|
||||
:action-message-variant="actionMessageVariant"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onCopyDownloadLink="copyDownloadLinkFromRecord"
|
||||
@onReimport="reimportFromRecord"
|
||||
@onActionMessageDismissed="onActionMessageDismissedFromRecord" />
|
||||
<ExportRecordTable
|
||||
v-if="hasPreviousExports"
|
||||
id="previous-export-records"
|
||||
:records="previousExportRecords"
|
||||
class="mt-3"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onReimport="reimportFromRecord" />
|
||||
</BCard>
|
||||
</div>
|
||||
<BAlert v-else id="no-export-records-alert" variant="info" class="mt-3" show>
|
||||
{{ availableRecordsMessage }}
|
||||
</BAlert>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.zenodo-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<GModal
|
||||
:show.sync="showOldRecords"
|
||||
:title="`Previous Export Records for ${historyName}`"
|
||||
size="medium"
|
||||
hide-header
|
||||
fixed-height
|
||||
class="history-export-old-records-modal"
|
||||
centered>
|
||||
<ExportRecordTable
|
||||
v-if="hasPreviousExports"
|
||||
id="previous-export-records"
|
||||
:records="previousExportRecords"
|
||||
class="mt-3"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onReimport="reimportFromRecord" />
|
||||
</GModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
import { createTestingPinia } from "@pinia/testing";
|
||||
import { getLocalVue } from "@tests/jest/helpers";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { setActivePinia } from "pinia";
|
||||
|
||||
import { useServerMock } from "@/api/client/__mocks__";
|
||||
import type { BrowsableFilesSourcePlugin } from "@/api/remoteFiles";
|
||||
|
||||
import HistoryExportWizard from "./HistoryExportWizard.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
const FAKE_HISTORY_ID = "fake-history-id";
|
||||
const FAKE_HISTORY_NAME = "Test History";
|
||||
|
||||
const REMOTE_FILES_API_RESPONSE: BrowsableFilesSourcePlugin[] = [
|
||||
{
|
||||
id: "test-posix-source",
|
||||
type: "posix",
|
||||
label: "TestSource",
|
||||
doc: "For testing",
|
||||
writable: true,
|
||||
browsable: true,
|
||||
requires_roles: undefined,
|
||||
requires_groups: undefined,
|
||||
supports: {
|
||||
pagination: false,
|
||||
search: false,
|
||||
sorting: false,
|
||||
},
|
||||
uri_root: "gxfiles://test-posix-source",
|
||||
},
|
||||
];
|
||||
|
||||
const ZENODO_PLUGIN: BrowsableFilesSourcePlugin = {
|
||||
id: "zenodo",
|
||||
type: "rdm",
|
||||
label: "Zenodo",
|
||||
doc: "For testing",
|
||||
writable: true,
|
||||
browsable: true,
|
||||
supports: {
|
||||
pagination: false,
|
||||
search: false,
|
||||
sorting: false,
|
||||
},
|
||||
uri_root: "zenodo://",
|
||||
};
|
||||
|
||||
const USER_ZENODO_PLUGIN: BrowsableFilesSourcePlugin = {
|
||||
id: "998c5bba-b18f-4223-9c93-0f36fa2fdae8",
|
||||
type: "zenodo",
|
||||
label: "My Zenodo",
|
||||
doc: "My integration with Zenodo",
|
||||
browsable: true,
|
||||
writable: true,
|
||||
requires_roles: null,
|
||||
requires_groups: null,
|
||||
url: "https://zenodo.org/",
|
||||
supports: {
|
||||
pagination: true,
|
||||
search: true,
|
||||
sorting: false,
|
||||
},
|
||||
uri_root: "gxuserfiles://998c5bba-b18f-4223-9c93-0f36fa2fdae8",
|
||||
};
|
||||
|
||||
const selectors = {
|
||||
wizard: ".history-export-wizard",
|
||||
formatCard: "[data-history-export-format]",
|
||||
destinationCard: "[data-history-export-destination]",
|
||||
directoryInput: "#directory",
|
||||
fileNameInput: "#exported-file-name",
|
||||
includeFilesCheckbox: 'input[type="checkbox"]',
|
||||
submitButton: ".go-next-btn",
|
||||
nextButton: ".go-next-btn",
|
||||
previousButton: ".go-back-btn",
|
||||
errorAlert: ".alert-danger",
|
||||
} as const;
|
||||
|
||||
interface MountOptions {
|
||||
historyId?: string;
|
||||
historyName?: string;
|
||||
isBusy?: boolean;
|
||||
}
|
||||
|
||||
async function mountHistoryExportWizard(options: MountOptions = {}) {
|
||||
const { historyId = FAKE_HISTORY_ID, historyName = FAKE_HISTORY_NAME, isBusy = false } = options;
|
||||
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
|
||||
const wrapper = mount(HistoryExportWizard as object, {
|
||||
propsData: {
|
||||
historyId,
|
||||
historyName,
|
||||
isBusy,
|
||||
},
|
||||
localVue,
|
||||
pinia,
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
const { server, http } = useServerMock();
|
||||
|
||||
describe("HistoryExportWizard.vue", () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json([]);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe("Component Initialization", () => {
|
||||
it("should start with format selection step", async () => {
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
expect(wrapper.find(selectors.formatCard).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should display available export formats", async () => {
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
const formatCards = wrapper.findAll(selectors.formatCard);
|
||||
expect(formatCards.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Format Selection", () => {
|
||||
it("should display format options", async () => {
|
||||
const formats = [
|
||||
{ id: "rocrate.zip", label: "RO-Crate" },
|
||||
{ id: "tar.gz", label: "Compressed TGZ" },
|
||||
];
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
const formatCards = wrapper.findAll(selectors.formatCard);
|
||||
expect(formatCards.length).toBe(formats.length);
|
||||
for (const format of formats) {
|
||||
const card = wrapper.find(`[data-history-export-format="${format.id}"]`);
|
||||
expect(card.exists()).toBe(true);
|
||||
expect(card.text()).toContain(format.label);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Destination Selection", () => {
|
||||
it("should show download destination by default", async () => {
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate to destination step
|
||||
const nextButtonAfterFormat = wrapper.find(selectors.nextButton);
|
||||
if (nextButtonAfterFormat.exists()) {
|
||||
await nextButtonAfterFormat.trigger("click");
|
||||
}
|
||||
|
||||
const downloadCard = wrapper.find('[data-history-export-destination="download"]');
|
||||
expect(downloadCard.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should show remote source destination when file sources are available", async () => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json(REMOTE_FILES_API_RESPONSE);
|
||||
})
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate to destination step
|
||||
const nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
const remoteCard = wrapper.find('[data-history-export-destination="remote-source"]');
|
||||
expect(remoteCard.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should show Zenodo destination when Zenodo plugin is available", async () => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json([ZENODO_PLUGIN]);
|
||||
})
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate to destination step
|
||||
const nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
const zenodoCard = wrapper.find('[data-history-export-destination="zenodo-repository"]');
|
||||
expect(zenodoCard.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should prioritize user-defined Zenodo over default Zenodo", async () => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json([ZENODO_PLUGIN, USER_ZENODO_PLUGIN]);
|
||||
})
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate to destination step
|
||||
const nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
const zenodoCard = wrapper.find('[data-history-export-destination="zenodo-repository"]');
|
||||
if (zenodoCard.exists()) {
|
||||
// Should show user-defined Zenodo label
|
||||
expect(zenodoCard.text()).toContain("My Zenodo");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Remote Source Setup", () => {
|
||||
it("should show directory input when remote source is selected", async () => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json(REMOTE_FILES_API_RESPONSE);
|
||||
})
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate to destination step
|
||||
let nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Select remote source
|
||||
const remoteCard = wrapper.find('[data-history-export-destination="remote-source"]');
|
||||
if (remoteCard.exists()) {
|
||||
await remoteCard.trigger("click");
|
||||
}
|
||||
|
||||
// Navigate to setup step
|
||||
nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Check that the setup step is rendered with directory selection
|
||||
expect(wrapper.text()).toContain("Select a 'repository' to export history to.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Export Summary", () => {
|
||||
it("should display default file name placeholder", async () => {
|
||||
// Set up a remote file source to show file name input
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json(REMOTE_FILES_API_RESPONSE);
|
||||
})
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate through wizard steps to final step
|
||||
// Step 1: Select destination
|
||||
let nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Step 2: Select remote source to require file name
|
||||
const remoteCard = wrapper.find('[data-history-export-destination="remote-source"]');
|
||||
if (remoteCard.exists()) {
|
||||
await remoteCard.trigger("click");
|
||||
}
|
||||
|
||||
// Step 3: Navigate to setup step
|
||||
nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Simulate selecting a directory by triggering the input
|
||||
const directoryInput = wrapper.find('input[placeholder="Click to select directory"]');
|
||||
if (directoryInput.exists()) {
|
||||
await directoryInput.setValue("gxfiles://test-remote-source/test-directory");
|
||||
}
|
||||
|
||||
// Step 4: Navigate to final step
|
||||
nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Check if the file name input has the expected placeholder text
|
||||
const fileNameInput = wrapper.find(selectors.fileNameInput);
|
||||
expect(fileNameInput.exists()).toBe(true);
|
||||
expect(fileNameInput.attributes("placeholder")).toContain(FAKE_HISTORY_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Event Handling", () => {
|
||||
it("should emit onExport event when export is triggered", async () => {
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate through wizard steps to final step
|
||||
// Step 1: Select destination
|
||||
let nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Step 2: Select download destination to skip setup steps
|
||||
const downloadCard = wrapper.find('[data-history-export-destination="download"]');
|
||||
if (downloadCard.exists()) {
|
||||
await downloadCard.trigger("click");
|
||||
}
|
||||
|
||||
// Step 3: Navigate to final step
|
||||
nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
const exportButton = wrapper.find(selectors.submitButton);
|
||||
await exportButton.trigger("click");
|
||||
|
||||
expect(wrapper.emitted("onExport")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validation", () => {
|
||||
it("should validate remote source selection", async () => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json(REMOTE_FILES_API_RESPONSE);
|
||||
})
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate to destination step
|
||||
let nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Navigate to remote source setup
|
||||
const remoteCard = wrapper.find('[data-history-export-destination="remote-source"]');
|
||||
if (remoteCard.exists()) {
|
||||
await remoteCard.trigger("click");
|
||||
}
|
||||
|
||||
// Navigate to setup step
|
||||
nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Check for directory input presence
|
||||
const directoryInput = wrapper.find(selectors.directoryInput);
|
||||
expect(directoryInput.exists() || wrapper.text().includes("Select a 'repository'")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Step Navigation", () => {
|
||||
it("should show setup steps for remote source selection", async () => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json(REMOTE_FILES_API_RESPONSE);
|
||||
})
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate to destination step
|
||||
let nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Select remote source
|
||||
const remoteCard = wrapper.find('[data-history-export-destination="remote-source"]');
|
||||
if (remoteCard.exists()) {
|
||||
await remoteCard.trigger("click");
|
||||
}
|
||||
|
||||
// Navigate to setup step
|
||||
nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Should show setup for remote source
|
||||
expect(wrapper.find(selectors.directoryInput).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should show Zenodo setup when Zenodo is selected", async () => {
|
||||
server.use(
|
||||
http.get("/api/remote_files/plugins", ({ response }) => {
|
||||
return response(200).json([ZENODO_PLUGIN]);
|
||||
})
|
||||
);
|
||||
|
||||
const wrapper = await mountHistoryExportWizard();
|
||||
|
||||
// Navigate to destination step
|
||||
let nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Select Zenodo
|
||||
const zenodoCard = wrapper.find('[data-history-export-destination="zenodo-repository"]');
|
||||
if (zenodoCard.exists()) {
|
||||
await zenodoCard.trigger("click");
|
||||
}
|
||||
|
||||
// Navigate to setup step
|
||||
nextButton = wrapper.find(selectors.nextButton);
|
||||
if (nextButton.exists()) {
|
||||
await nextButton.trigger("click");
|
||||
}
|
||||
|
||||
// Should show Zenodo-specific setup
|
||||
expect(wrapper.text().includes("Zenodo") || wrapper.text().includes("draft record")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,396 @@
|
||||
<script setup lang="ts">
|
||||
import { BAlert, BCard, BCardGroup, BCardTitle, BFormCheckbox, BFormGroup, BFormInput } from "bootstrap-vue";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
|
||||
import { AVAILABLE_EXPORT_FORMATS } from "@/api/histories.export";
|
||||
import { useWizard } from "@/components/Common/Wizard/useWizard";
|
||||
import { borderVariant } from "@/components/Common/Wizard/utils";
|
||||
import { useFileSources } from "@/composables/fileSources";
|
||||
import { useMarkdown } from "@/composables/markdown";
|
||||
import { DEFAULT_EXPORT_PARAMS } from "@/composables/shortTermStorage";
|
||||
|
||||
import type { HistoryExportData, HistoryExportDestination } from "./types";
|
||||
|
||||
import RDMCredentialsInfo from "@/components/Common/RDMCredentialsInfo.vue";
|
||||
import RDMDestinationSelector from "@/components/Common/RDMDestinationSelector.vue";
|
||||
import GenericWizard from "@/components/Common/Wizard/GenericWizard.vue";
|
||||
import ExternalLink from "@/components/ExternalLink.vue";
|
||||
import FilesInput from "@/components/FilesDialog/FilesInput.vue";
|
||||
import FileSourceNameSpan from "@/components/FileSources/FileSourceNameSpan.vue";
|
||||
|
||||
const { renderMarkdown } = useMarkdown({ openLinksInNewPage: true });
|
||||
|
||||
const { hasWritable: hasWritableFileSources } = useFileSources({ exclude: ["rdm"] });
|
||||
const {
|
||||
hasWritable: hasWritableRDMFileSources,
|
||||
getFileSourceById,
|
||||
getFileSourcesByType,
|
||||
isPrivateFileSource,
|
||||
} = useFileSources({ include: ["rdm"] });
|
||||
|
||||
const resource = "history";
|
||||
|
||||
interface ExportDestinationInfo {
|
||||
destination: HistoryExportDestination;
|
||||
label: string;
|
||||
markdownDescription: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
historyId: string;
|
||||
historyName: string;
|
||||
isBusy: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onExport", exportData: HistoryExportData): void;
|
||||
}>();
|
||||
|
||||
const errorMessage = ref<string>();
|
||||
const exportData: HistoryExportData = reactive(initializeExportData());
|
||||
|
||||
const defaultFileName = computed(() => `(Galaxy History) ${props.historyName}`);
|
||||
|
||||
const exportButtonLabel = computed(() => {
|
||||
switch (exportData.destination) {
|
||||
case "download":
|
||||
return "Generate Download Link";
|
||||
case "remote-source":
|
||||
return "Export to Remote Source";
|
||||
case "rdm-repository":
|
||||
return "Export to RDM Repository";
|
||||
case "zenodo-repository":
|
||||
return "Export to Zenodo";
|
||||
default:
|
||||
return "Export";
|
||||
}
|
||||
});
|
||||
|
||||
const needsFileName = computed(
|
||||
() =>
|
||||
exportData.destination === "remote-source" ||
|
||||
exportData.destination === "rdm-repository" ||
|
||||
exportData.destination === "zenodo-repository"
|
||||
);
|
||||
|
||||
const exportDestinationSummary = computed(() => {
|
||||
const exportDestination = exportDestinationTargets.value.find(
|
||||
(target) => target.destination === exportData.destination
|
||||
);
|
||||
return exportDestination?.label ?? "Unknown Destination";
|
||||
});
|
||||
|
||||
const exportDestinationTargets = computed(initializeExportDestinations);
|
||||
|
||||
const exportFormats = computed(() => AVAILABLE_EXPORT_FORMATS);
|
||||
|
||||
const exportFormatName = computed(() => {
|
||||
const format = exportFormats.value.find((f) => f.id === exportData.modelStoreFormat);
|
||||
return format ? format.name : "";
|
||||
});
|
||||
|
||||
const zenodoSource = computed(() => getZenodoSource());
|
||||
|
||||
const wizard = useWizard({
|
||||
"select-format": {
|
||||
label: "Select output format",
|
||||
instructions: computed(() => {
|
||||
return `Select the format you would like to export the ${resource} to and click Next to continue.`;
|
||||
}),
|
||||
isValid: () => true,
|
||||
isSkippable: () => false,
|
||||
},
|
||||
"select-destination": {
|
||||
label: "Select destination",
|
||||
instructions: computed(() => {
|
||||
return `Select where you would like to export the ${resource} ${exportFormatName.value} to and click Next to continue.`;
|
||||
}),
|
||||
isValid: () => true,
|
||||
isSkippable: () => false,
|
||||
},
|
||||
"setup-remote": {
|
||||
label: "Select remote source",
|
||||
instructions: "Select remote source directory",
|
||||
isValid: () => Boolean(exportData.remoteUri),
|
||||
isSkippable: () => exportData.destination !== "remote-source",
|
||||
},
|
||||
"setup-rdm": {
|
||||
label: "Select RDM repository",
|
||||
instructions: "Select RDM repository and provide a draft record to export to",
|
||||
isValid: () => Boolean(exportData.remoteUri),
|
||||
isSkippable: () => exportData.destination !== "rdm-repository",
|
||||
},
|
||||
"setup-zenodo": {
|
||||
label: "Select draft record",
|
||||
instructions: "Select or create a draft record to export to",
|
||||
isValid: () => Boolean(exportData.remoteUri),
|
||||
isSkippable: () => exportData.destination !== "zenodo-repository",
|
||||
},
|
||||
"export-summary": {
|
||||
label: "Export",
|
||||
instructions: "Summary",
|
||||
isValid: () => Boolean(exportData.outputFileName) || exportData.destination === "download",
|
||||
isSkippable: () => false,
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isBusy,
|
||||
(newValue, oldValue) => {
|
||||
if (oldValue && !newValue) {
|
||||
resetWizard();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function getZenodoSource() {
|
||||
const zenodoSources = getFileSourcesByType("zenodo");
|
||||
// Prioritize the one provided by the user in case there are multiple
|
||||
return zenodoSources.find((fs) => isPrivateFileSource(fs) && fs.writable) ?? getFileSourceById("zenodo");
|
||||
}
|
||||
|
||||
function onRecordSelected(recordUri: string) {
|
||||
exportData.remoteUri = recordUri;
|
||||
}
|
||||
|
||||
async function exportHistory() {
|
||||
emit("onExport", exportData);
|
||||
}
|
||||
|
||||
function initializeExportDestinations(): ExportDestinationInfo[] {
|
||||
const destinations: ExportDestinationInfo[] = [
|
||||
{
|
||||
destination: "download",
|
||||
label: "Temporary Direct Download",
|
||||
markdownDescription: `Generate a link to the export file and download it directly to your computer.
|
||||
|
||||
**Please note that the link will expire after 24 hours.**`,
|
||||
},
|
||||
];
|
||||
|
||||
if (hasWritableFileSources.value) {
|
||||
destinations.push({
|
||||
destination: "remote-source",
|
||||
label: "Repository",
|
||||
markdownDescription: `If you need a **more permanent** way of storing your ${resource} you can export it directly to one of the available repositories. You will be able to re-import it later as long as it remains available on the remote server.
|
||||
|
||||
Examples of remote sources include Amazon S3, Azure Storage, Google Drive... and other public or personal file sources that you have setup access to.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWritableRDMFileSources.value) {
|
||||
destinations.push({
|
||||
destination: "rdm-repository",
|
||||
label: "RDM Repository",
|
||||
markdownDescription: `You can upload your ${resource} to one of the available **Research Data Management repositories** here.
|
||||
This will allow you to easily associate your ${resource} with your research project or publication.
|
||||
|
||||
Examples of RDM repositories include [Invenio RDM](https://inveniosoftware.org/products/rdm/) instances, [RSpace](https://www.researchspace.com/), and other public or personal repositories that you have setup access to.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (zenodoSource.value) {
|
||||
destinations.push({
|
||||
destination: "zenodo-repository",
|
||||
label: `${zenodoSource.value.label}`,
|
||||
markdownDescription: `
|
||||
|
||||
[Zenodo](https://zenodo.org/) is a general-purpose open repository developed under the [European OpenAIRE](https://www.openaire.eu/) program and operated by [CERN](https://home.cern/). It allows researchers to deposit research papers, data sets, research software, reports, and any other research related digital artefacts. For each submission, a persistent **digital object identifier (DOI)** is minted, which makes the stored items easily citeable.`,
|
||||
});
|
||||
}
|
||||
|
||||
return destinations;
|
||||
}
|
||||
|
||||
function initializeExportData(): HistoryExportData {
|
||||
return {
|
||||
...DEFAULT_EXPORT_PARAMS,
|
||||
destination: "download",
|
||||
remoteUri: "",
|
||||
outputFileName: "",
|
||||
};
|
||||
}
|
||||
|
||||
function resetWizard() {
|
||||
const initialExportData = initializeExportData();
|
||||
Object.assign(exportData, initialExportData);
|
||||
wizard.goTo("select-format");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BAlert v-if="errorMessage" show dismissible fade variant="danger" @dismissed="errorMessage = undefined">
|
||||
{{ errorMessage }}
|
||||
</BAlert>
|
||||
<GenericWizard
|
||||
class="history-export-wizard"
|
||||
:use="wizard"
|
||||
:submit-button-label="exportButtonLabel"
|
||||
:is-busy="isBusy"
|
||||
@submit="exportHistory">
|
||||
<div v-if="wizard.isCurrent('select-format')">
|
||||
<BCardGroup deck>
|
||||
<BCard
|
||||
v-for="format in exportFormats"
|
||||
:key="format.id"
|
||||
:data-history-export-format="format.id"
|
||||
class="wizard-selection-card"
|
||||
:border-variant="borderVariant(exportData.modelStoreFormat == format.id)"
|
||||
@click="exportData.modelStoreFormat = format.id">
|
||||
<BCardTitle>
|
||||
<b>{{ format.name }}</b>
|
||||
</BCardTitle>
|
||||
<div v-if="format.id === 'rocrate.zip'">
|
||||
<p>
|
||||
RO-Crate is a community effort to establish a lightweight approach to packaging research
|
||||
data with their metadata. It makes use of schema.org annotations in JSON-LD format.
|
||||
</p>
|
||||
<img
|
||||
class="card-img"
|
||||
src="https://www.researchobject.org/ro-crate/assets/img/ro-crate-wide.svg"
|
||||
alt="RO-Crate Logo" />
|
||||
<br />
|
||||
<ExternalLink href="https://www.researchobject.org/ro-crate/">
|
||||
<b>Learn more</b>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>A compressed tar.gz archive containing the history and its metadata.</p>
|
||||
</div>
|
||||
</BCard>
|
||||
</BCardGroup>
|
||||
</div>
|
||||
|
||||
<div v-if="wizard.isCurrent('select-destination')">
|
||||
<BCardGroup deck>
|
||||
<BCard
|
||||
v-for="target in exportDestinationTargets"
|
||||
:key="target.destination"
|
||||
:data-history-export-destination="target.destination"
|
||||
:border-variant="borderVariant(exportData.destination === target.destination)"
|
||||
:header-bg-variant="exportData.destination === target.destination ? 'primary' : 'default'"
|
||||
:header-text-variant="exportData.destination === target.destination ? 'white' : 'default'"
|
||||
:header="target.label"
|
||||
class="wizard-selection-card"
|
||||
@click="exportData.destination = target.destination">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="renderMarkdown(target.markdownDescription)" />
|
||||
</BCard>
|
||||
</BCardGroup>
|
||||
</div>
|
||||
|
||||
<div v-if="wizard.isCurrent('setup-remote')">
|
||||
<BFormGroup
|
||||
id="fieldset-directory"
|
||||
label-for="directory"
|
||||
:description="`Select a 'repository' to export ${resource} to.`"
|
||||
class="mt-3">
|
||||
<FilesInput
|
||||
id="directory"
|
||||
v-model="exportData.remoteUri"
|
||||
mode="directory"
|
||||
:require-writable="true"
|
||||
:filter-options="{ exclude: ['rdm'] }" />
|
||||
</BFormGroup>
|
||||
</div>
|
||||
|
||||
<div v-if="wizard.isCurrent('setup-rdm')">
|
||||
<RDMCredentialsInfo what="history export archive" />
|
||||
|
||||
<RDMDestinationSelector :what="resource" @onRecordSelected="onRecordSelected" />
|
||||
</div>
|
||||
|
||||
<div v-if="wizard.isCurrent('setup-zenodo')">
|
||||
<div class="zenodo-info">
|
||||
<img
|
||||
class="card-img"
|
||||
src="https://raw.githubusercontent.com/zenodo/zenodo/master/zenodo/modules/theme/static/img/logos/zenodo-gradient-square.svg"
|
||||
alt="ZENODO Logo" />
|
||||
|
||||
<div>
|
||||
<RDMCredentialsInfo
|
||||
what="history export archive"
|
||||
:selected-repository="zenodoSource"
|
||||
:is-private-file-source="zenodoSource ? isPrivateFileSource(zenodoSource) : false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RDMDestinationSelector
|
||||
:what="resource"
|
||||
:file-source="zenodoSource"
|
||||
@onRecordSelected="onRecordSelected" />
|
||||
</div>
|
||||
|
||||
<div v-if="wizard.isCurrent('export-summary')">
|
||||
<BFormGroup
|
||||
v-if="needsFileName"
|
||||
label-for="exported-file-name"
|
||||
:description="`Give the exported file a name.`"
|
||||
class="mt-3">
|
||||
<BFormInput
|
||||
id="exported-file-name"
|
||||
v-model="exportData.outputFileName"
|
||||
:placeholder="defaultFileName"
|
||||
required />
|
||||
</BFormGroup>
|
||||
|
||||
<BFormGroup label="Dataset files included in the package:">
|
||||
<BFormCheckbox v-model="exportData.includeFiles" switch> Include Active Files </BFormCheckbox>
|
||||
|
||||
<BFormCheckbox v-model="exportData.includeDeleted" switch>
|
||||
Include Deleted (not purged)
|
||||
</BFormCheckbox>
|
||||
|
||||
<BFormCheckbox v-model="exportData.includeHidden" switch> Include Hidden </BFormCheckbox>
|
||||
</BFormGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
Format <b>{{ exportFormatName }}</b>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Destination
|
||||
<b>{{ exportDestinationSummary }}</b>
|
||||
<b v-if="exportData.destination !== 'download' && exportData.remoteUri">
|
||||
<FileSourceNameSpan :uri="exportData.remoteUri" class="text-primary" />
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<div v-if="needsFileName && exportData.outputFileName">
|
||||
File Name <b>{{ exportData.outputFileName }}.{{ exportData.modelStoreFormat }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</GenericWizard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.zenodo-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.wizard-selection-card {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
border-color: #007bff;
|
||||
}
|
||||
}
|
||||
|
||||
.card-img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-height: 100px;
|
||||
max-width: 100%;
|
||||
max-inline-size: -webkit-fill-available;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ExportParams } from "@/components/Common/models/exportRecordModel";
|
||||
|
||||
export type HistoryExportDestination = "download" | "remote-source" | "rdm-repository" | "zenodo-repository";
|
||||
|
||||
export interface HistoryExportData extends ExportParams {
|
||||
destination: HistoryExportDestination;
|
||||
remoteUri: string;
|
||||
outputFileName: string;
|
||||
}
|
||||
@@ -521,25 +521,25 @@ history_export:
|
||||
|
||||
history_export_tasks:
|
||||
selectors:
|
||||
direct_download: '.gen-direct-download-btn'
|
||||
file_source_tab: '.tab-export-to-file'
|
||||
remote_file_name_input: '#file-source-tab #name'
|
||||
toggle_options_link: '#toggle-options-link'
|
||||
export_format_selector: '#format-selector'
|
||||
select_format:
|
||||
type: xpath
|
||||
selector: '//select[@id="format-selector"]/option[@value="${format}"]'
|
||||
history_export_wizard: '.history-export-wizard'
|
||||
next_button: '.go-next-btn'
|
||||
back_button: '.go-back-btn'
|
||||
export_button: '.go-next-btn.btn-primary'
|
||||
select_format: '[data-history-export-format="${format}"]'
|
||||
select_destination: '[data-history-export-destination="${destination}"]'
|
||||
exported_file_name: '#exported-file-name'
|
||||
|
||||
last_export_record:
|
||||
selectors:
|
||||
details: '.export-record-details'
|
||||
preparing_export: '.export-record-details .loading-icon'
|
||||
export_format: '.record-archive-format'
|
||||
up_to_date_icon: '.record-up-to-date-icon'
|
||||
outdated_icon: '.record-outdated-icon'
|
||||
expiration_warning_icon: '.record-expiration-warning-icon'
|
||||
download_btn: '.record-download-btn'
|
||||
reimport_btn: '.record-reimport-btn'
|
||||
preparing_export_badge: '#g-card-badge-in-progress-latest-export-record'
|
||||
download_ready_badge: '#g-card-badge-ready-to-download-latest-export-record'
|
||||
expiration_warning_badge: '#g-card-badge-expires-soon-latest-export-record'
|
||||
download_btn: '#g-card-action-download-latest-export-record'
|
||||
reimport_btn: '#g-card-action-reimport-latest-export-record'
|
||||
|
||||
history_import:
|
||||
selectors:
|
||||
|
||||
@@ -102,22 +102,42 @@ class TestHistoryImportExportFtpSeleniumIntegrationWithTasks(TestHistoryImportEx
|
||||
|
||||
# Export to direct download link
|
||||
export_format = "rocrate.zip"
|
||||
destination = "download"
|
||||
history_export_tasks = self.components.history_export_tasks
|
||||
history_export_tasks.direct_download.wait_for_and_click()
|
||||
# Select export format
|
||||
history_export_tasks.select_format(format=export_format).wait_for_and_click()
|
||||
history_export_tasks.next_button.wait_for_and_click()
|
||||
# Select destination
|
||||
history_export_tasks.select_destination(destination=destination).wait_for_and_click()
|
||||
history_export_tasks.next_button.wait_for_and_click()
|
||||
# Confirm export
|
||||
history_export_tasks.export_button.wait_for_and_click()
|
||||
|
||||
self._verify_last_export_record(expected_format=export_format, is_download=True)
|
||||
|
||||
# Change export format
|
||||
export_format = "tar.gz"
|
||||
history_export_tasks.toggle_options_link.wait_for_and_click()
|
||||
history_export_tasks.export_format_selector.wait_for_visible()
|
||||
destination = "remote-source"
|
||||
history_export_tasks.select_format(format=export_format).wait_for_and_click()
|
||||
history_export_tasks.toggle_options_link.wait_for_and_click()
|
||||
history_export_tasks.next_button.wait_for_and_click()
|
||||
# Select destination
|
||||
history_export_tasks.select_destination(destination=destination).wait_for_and_click()
|
||||
history_export_tasks.next_button.wait_for_and_click()
|
||||
|
||||
# Export to FTP file source
|
||||
history_export_tasks.file_source_tab.wait_for_present()
|
||||
history_export_tasks.file_source_tab.wait_for_and_click()
|
||||
self._export_to_ftp_with_filename("my_export.tar.gz")
|
||||
# Select FTP file source
|
||||
self.components.history_export.directory_input.wait_for_and_click()
|
||||
self.components.files_dialog.ftp_label.wait_for_and_click()
|
||||
self.components.upload.file_dialog_ok.wait_for_and_click()
|
||||
|
||||
# Go to Summary step
|
||||
history_export_tasks.next_button.wait_for_and_click()
|
||||
|
||||
# Set filename
|
||||
filename = "my_export.tar.gz"
|
||||
history_export_tasks.exported_file_name.wait_for_and_send_keys(filename)
|
||||
|
||||
# Confirm export
|
||||
history_export_tasks.export_button.wait_for_and_click()
|
||||
|
||||
self._verify_last_export_record(expected_format=export_format)
|
||||
|
||||
@@ -125,8 +145,8 @@ class TestHistoryImportExportFtpSeleniumIntegrationWithTasks(TestHistoryImportEx
|
||||
self, expected_format: str, expect_up_to_date: bool = True, is_download: bool = False
|
||||
):
|
||||
last_export_record = self.components.last_export_record
|
||||
last_export_record.preparing_export.wait_for_visible()
|
||||
last_export_record.preparing_export.wait_for_absent(wait_type=self.wait_types.DATABASE_OPERATION)
|
||||
last_export_record.preparing_export_badge.wait_for_visible()
|
||||
last_export_record.preparing_export_badge.wait_for_absent(wait_type=self.wait_types.DATABASE_OPERATION)
|
||||
|
||||
last_export_record.details.wait_for_visible()
|
||||
format_element = last_export_record.export_format.wait_for_visible()
|
||||
@@ -138,7 +158,7 @@ class TestHistoryImportExportFtpSeleniumIntegrationWithTasks(TestHistoryImportEx
|
||||
last_export_record.outdated_icon.wait_for_visible()
|
||||
|
||||
if is_download:
|
||||
last_export_record.expiration_warning_icon.wait_for_visible()
|
||||
last_export_record.expiration_warning_badge.wait_for_visible()
|
||||
last_export_record.download_btn.wait_for_visible()
|
||||
else:
|
||||
last_export_record.reimport_btn.wait_for_visible()
|
||||
|
||||
Reference in New Issue
Block a user