Skip to content

Commit

Permalink
Merge pull request galaxyproject#18796 from davelopez/explore_export_…
Browse files Browse the repository at this point in the history
…invocation_wizard

Add wizard-like export view for workflow invocations
  • Loading branch information
itisAliRH authored Sep 19, 2024
2 parents 9da413c + c9bea5a commit 83a63e7
Show file tree
Hide file tree
Showing 26 changed files with 1,466 additions and 743 deletions.
176 changes: 32 additions & 144 deletions client/src/components/Common/ExportRDMForm.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
<script setup lang="ts">
import { BButton, BCard, BFormGroup, BFormInput, BFormRadio, BFormRadioGroup } from "bootstrap-vue";
import { BButton, BFormGroup, BFormInput } from "bootstrap-vue";
import { computed, ref } from "vue";
import { GalaxyApi } from "@/api";
import { type BrowsableFilesSourcePlugin, type CreatedEntry, type FilterFileSourcesOptions } from "@/api/remoteFiles";
import { useToast } from "@/composables/toast";
import { type BrowsableFilesSourcePlugin } from "@/api/remoteFiles";
import localize from "@/utils/localization";
import { errorMessageAsString } from "@/utils/simple-error";
import { fileSourcePluginToItem } from "../FilesDialog/utilities";
import ExternalLink from "@/components/ExternalLink.vue";
import FilesInput from "@/components/FilesDialog/FilesInput.vue";
const toast = useToast();
import RDMDestinationSelector from "@/components/Common/RDMDestinationSelector.vue";
interface Props {
what?: string;
Expand All @@ -39,31 +31,17 @@ const emit = defineEmits<{
(e: "export", recordUri: string, fileName: string, newRecordName?: string): void;
}>();
type ExportChoice = "existing" | "new";
const includeOnlyRDMCompatible: FilterFileSourcesOptions = { include: ["rdm"] };
const destinationSelector = ref<InstanceType<typeof RDMDestinationSelector>>();
const recordUri = ref<string>("");
const sourceUri = ref<string>(props.fileSource?.uri_root ?? "");
const fileName = ref<string>(props.defaultFilename);
const exportChoice = ref<ExportChoice>("new");
const recordName = ref<string>(props.defaultRecordName);
const newEntry = ref<CreatedEntry>();
const canCreateRecord = computed(() => Boolean(sourceUri.value) && Boolean(recordName.value));
const isNewRecord = ref(true);
const canExport = computed(() => Boolean(recordUri.value) && Boolean(fileName.value));
const repositoryRecordDescription = computed(() => localize(`Select a repository to export ${props.what} to.`));
const nameDescription = computed(() => localize("Give the exported file a name."));
const recordNameDescription = computed(() => localize("Give the new record a name or title."));
const namePlaceholder = computed(() => localize("File name"));
const recordNamePlaceholder = computed(() => localize("Record name"));
const uniqueSourceId = computed(() => props.fileSource?.id ?? "any");
const fileSourceAsItem = computed(() => (props.fileSource ? fileSourcePluginToItem(props.fileSource) : undefined));
function doExport() {
emit("export", recordUri.value, fileName.value);
Expand All @@ -73,28 +51,16 @@ function doExport() {
}
}
async function doCreateRecord() {
const { data, error } = await GalaxyApi().POST("/api/remote_files", {
body: {
target: sourceUri.value,
name: recordName.value,
},
});
if (error) {
toast.error(errorMessageAsString(error));
return;
}
newEntry.value = data;
recordUri.value = newEntry.value.uri;
}
function clearInputs() {
recordUri.value = "";
sourceUri.value = props.fileSource?.uri_root ?? "";
fileName.value = "";
newEntry.value = undefined;
//@ts-ignore incorrect property not found error
destinationSelector.value?.reset();
}
function onRecordSelected(selectedRecordUri: string) {
recordUri.value = selectedRecordUri;
}
</script>

Expand All @@ -104,106 +70,28 @@ function clearInputs() {
<BFormInput id="file-name-input" v-model="fileName" :placeholder="namePlaceholder" required />
</BFormGroup>

<p>
Your {{ what }} needs to be uploaded to an existing <i>draft</i> record. You will need to create a
<b>new record</b> or select an existing <b>draft record</b> and then export your {{ what }} to it.
</p>

<BFormRadioGroup v-model="exportChoice" class="export-radio-group">
<BFormRadio :id="`radio-new-${uniqueSourceId}`" v-localize name="exportChoice" value="new">
Export to new record
</BFormRadio>

<BFormRadio :id="`radio-existing-${uniqueSourceId}`" v-localize name="exportChoice" value="existing">
Export to existing draft record
</BFormRadio>
</BFormRadioGroup>

<div v-if="exportChoice === 'new'">
<div v-if="newEntry">
<BCard>
<p>
<b>{{ newEntry.name }}</b>
<span v-localize> draft record has been created in the repository.</span>
</p>

<p v-if="newEntry.external_link">
You can preview the record in the repository, further edit its metadata and decide when to
publish it at
<ExternalLink :href="newEntry.external_link">
<b>{{ newEntry.external_link }}</b>
</ExternalLink>
</p>

<p v-localize>Please use the button below to upload the exported {{ props.what }} to the record.</p>

<BButton
id="export-button-new-record"
v-localize
class="export-button"
variant="primary"
:disabled="!canExport"
@click.prevent="doExport">
Export to this record
</BButton>
</BCard>
</div>
<div v-else>
<BFormGroup
v-if="!props.fileSource"
id="fieldset-record-new"
label-for="source-selector"
:description="repositoryRecordDescription"
class="mt-3">
<FilesInput
id="source-selector"
v-model="sourceUri"
mode="source"
:require-writable="true"
:filter-options="includeOnlyRDMCompatible" />
</BFormGroup>

<BFormGroup
id="fieldset-record-name"
label-for="record-name"
:description="recordNameDescription"
class="mt-3">
<BFormInput
id="record-name-input"
v-model="recordName"
:placeholder="recordNamePlaceholder"
required />
</BFormGroup>

<p v-localize>
You need to create the new record in a repository before exporting the {{ props.what }} to it.
</p>

<BButton
id="create-record-button"
v-localize
variant="primary"
:disabled="!canCreateRecord"
@click.prevent="doCreateRecord">
Create new record
</BButton>
</div>
<RDMDestinationSelector
ref="destinationSelector"
:what="what"
:file-source="fileSource"
@onRecordSelected="onRecordSelected" />

<div v-if="isNewRecord">
<p v-localize class="mt-3">
Please use the button below to upload the exported {{ props.what }} to the record.
</p>

<BButton
id="export-button-new-record"
v-localize
class="export-button"
variant="primary"
:disabled="!canExport"
@click.prevent="doExport">
Export to this record
</BButton>
</div>
<div v-else>
<BFormGroup
id="fieldset-record-existing"
label-for="existing-record-selector"
:description="repositoryRecordDescription"
class="mt-3">
<FilesInput
id="existing-record-selector"
v-model="recordUri"
mode="directory"
:require-writable="true"
:filter-options="fileSource ? undefined : includeOnlyRDMCompatible"
:selected-item="fileSourceAsItem" />
</BFormGroup>

<BButton
id="export-button-existing-record"
v-localize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import PersistentTaskProgressMonitorAlert from "@/components/Common/PersistentTa

type ComponentUnderTestProps = Partial<PropType<typeof PersistentTaskProgressMonitorAlert>>;

const selectors = {
ProgressAlert: ".progress-monitor-alert",
} as const;

const FAKE_MONITOR_REQUEST: MonitoringRequest = {
source: "test",
action: "testing",
Expand Down Expand Up @@ -54,7 +58,7 @@ describe("PersistentTaskProgressMonitorAlert.vue", () => {

it("does not render when no monitoring data is available", () => {
const wrapper = mountComponent();
expect(wrapper.find(".d-flex").exists()).toBe(false);
expect(wrapper.find(selectors.ProgressAlert).exists()).toBe(false);
});

it("renders in progress when monitoring data is available and in progress", () => {
Expand All @@ -75,7 +79,7 @@ describe("PersistentTaskProgressMonitorAlert.vue", () => {
useMonitor,
});

expect(wrapper.find(".d-flex").exists()).toBe(true);
expect(wrapper.find(selectors.ProgressAlert).exists()).toBe(true);

const inProgressAlert = wrapper.find('[variant="info"]');
expect(inProgressAlert.exists()).toBe(true);
Expand All @@ -100,7 +104,7 @@ describe("PersistentTaskProgressMonitorAlert.vue", () => {
useMonitor,
});

expect(wrapper.find(".d-flex").exists()).toBe(true);
expect(wrapper.find(selectors.ProgressAlert).exists()).toBe(true);

const completedAlert = wrapper.find('[variant="success"]');
expect(completedAlert.exists()).toBe(true);
Expand All @@ -125,7 +129,7 @@ describe("PersistentTaskProgressMonitorAlert.vue", () => {
useMonitor,
});

expect(wrapper.find(".d-flex").exists()).toBe(true);
expect(wrapper.find(selectors.ProgressAlert).exists()).toBe(true);

const failedAlert = wrapper.find('[variant="danger"]');
expect(failedAlert.exists()).toBe(true);
Expand Down Expand Up @@ -155,7 +159,7 @@ describe("PersistentTaskProgressMonitorAlert.vue", () => {
useMonitor,
});

expect(wrapper.find(".d-flex").exists()).toBe(true);
expect(wrapper.find(selectors.ProgressAlert).exists()).toBe(true);

const completedAlert = wrapper.find('[variant="success"]');
expect(completedAlert.exists()).toBe(true);
Expand Down Expand Up @@ -184,7 +188,7 @@ describe("PersistentTaskProgressMonitorAlert.vue", () => {
useMonitor,
});

expect(wrapper.find(".d-flex").exists()).toBe(true);
expect(wrapper.find(selectors.ProgressAlert).exists()).toBe(true);

const completedAlert = wrapper.find('[variant="success"]');
expect(completedAlert.exists()).toBe(true);
Expand All @@ -209,7 +213,7 @@ describe("PersistentTaskProgressMonitorAlert.vue", () => {
useMonitor,
});

expect(wrapper.find(".d-flex").exists()).toBe(true);
expect(wrapper.find(selectors.ProgressAlert).exists()).toBe(true);

const warningAlert = wrapper.find('[variant="warning"]');
expect(warningAlert.exists()).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BLink } from "bootstrap-vue";
Expand All @@ -9,10 +8,9 @@ import { type TaskMonitor } from "@/composables/genericTaskMonitor";
import { type MonitoringRequest, usePersistentProgressTaskMonitor } from "@/composables/persistentProgressMonitor";
import { useShortTermStorage } from "@/composables/shortTermStorage";
import FileSourceNameSpan from "@/components/FileSources/FileSourceNameSpan.vue";
import UtcDate from "@/components/UtcDate.vue";
library.add(faSpinner);
interface Props {
monitorRequest: MonitoringRequest;
useMonitor: TaskMonitor;
Expand Down Expand Up @@ -46,6 +44,10 @@ const props = withDefaults(defineProps<Props>(), {
requestFailedMessage: "Request failed.",
});
const emit = defineEmits<{
(e: "onDismiss"): void;
}>();
const { getDownloadObjectUrl } = useShortTermStorage();
const {
Expand All @@ -58,6 +60,7 @@ const {
status,
hasExpired,
expirationDate,
monitoringData,
start,
reset,
} = usePersistentProgressTaskMonitor(props.monitorRequest, props.useMonitor);
Expand All @@ -71,6 +74,10 @@ const downloadUrl = computed(() => {
return undefined;
});
const remoteUri = computed(() => {
return monitoringData.value?.request.remoteUri;
});
if (hasMonitoringData.value) {
start();
}
Expand Down Expand Up @@ -102,11 +109,12 @@ watch(
function dismissAlert() {
reset();
emit("onDismiss");
}
</script>

<template>
<div v-if="hasMonitoringData" class="d-flex justify-content-end">
<div v-if="hasMonitoringData" class="progress-monitor-alert">
<BAlert v-if="hasExpired" variant="warning" show dismissible @dismissed="dismissAlert">
The {{ monitorRequest.action }} task has <b>expired</b> and the result is no longer available.
</BAlert>
Expand All @@ -116,10 +124,18 @@ function dismissAlert() {
</BAlert>
<BAlert v-else-if="isCompleted" variant="success" show dismissible @dismissed="dismissAlert">
<span>{{ completedMessage }}</span>

<BLink v-if="downloadUrl" class="download-link" :href="downloadUrl">
<b>Download here</b>
</BLink>

<span v-if="remoteUri">
The result should be available at
<b><FileSourceNameSpan :uri="remoteUri" :show-full-uri="true" /></b>
</span>

<br />

<span v-if="expirationDate">
This result will <b>expire <UtcDate :date="expirationDate.toISOString()" mode="elapsed" /></b>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import { RouterLink } from "vue-router";
interface Props {
selectedRepository?: string;
what?: string;
}
withDefaults(defineProps<Props>(), {
selectedRepository: "the selected repository",
what: "file",
});
</script>

Expand Down
Loading

0 comments on commit 83a63e7

Please sign in to comment.