Skip to content

Commit 9dfe91f

Browse files
committed
Implement /api/exports for listing recent local/remote exports
1 parent 4fc7cfc commit 9dfe91f

14 files changed

Lines changed: 801 additions & 42 deletions

File tree

client/src/api/exports.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { GalaxyApi, type ObjectExportTaskResponse } from "@/api";
2+
import { ExportRecordModel } from "@/components/Common/models/exportRecordModel";
3+
import { rethrowSimple } from "@/utils/simple-error";
4+
5+
/**
6+
* Gets a list of recent export records for the current user.
7+
* This includes exports to remote file sources (not short-term storage downloads).
8+
* @param limit Maximum number of exports to return
9+
* @param days Number of days to look back
10+
* @returns A promise with a list of export records for the current user.
11+
*/
12+
export async function fetchUserExportRecords(limit = 50, days = 30) {
13+
const { data, error } = await GalaxyApi().GET("/api/exports", {
14+
params: {
15+
query: { limit, days },
16+
},
17+
});
18+
19+
if (error) {
20+
rethrowSimple(error);
21+
}
22+
23+
return data.map((item: ObjectExportTaskResponse) => new ExportRecordModel(item));
24+
}

client/src/api/schema/schema.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,26 @@ export interface paths {
12521252
patch?: never;
12531253
trace?: never;
12541254
};
1255+
"/api/exports": {
1256+
parameters: {
1257+
query?: never;
1258+
header?: never;
1259+
path?: never;
1260+
cookie?: never;
1261+
};
1262+
/**
1263+
* Get recent exports for the current user.
1264+
* @description Returns a list of recent exports (to remote file sources) for the current user.
1265+
*/
1266+
get: operations["index_api_exports_get"];
1267+
put?: never;
1268+
post?: never;
1269+
delete?: never;
1270+
options?: never;
1271+
head?: never;
1272+
patch?: never;
1273+
trace?: never;
1274+
};
12551275
"/api/file_landings": {
12561276
parameters: {
12571277
query?: never;
@@ -28435,6 +28455,52 @@ export interface operations {
2843528455
};
2843628456
};
2843728457
};
28458+
index_api_exports_get: {
28459+
parameters: {
28460+
query?: {
28461+
/** @description Maximum number of exports to return. */
28462+
limit?: number | null;
28463+
/** @description Number of days to look back. */
28464+
days?: number;
28465+
};
28466+
header?: {
28467+
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
28468+
"run-as"?: string | null;
28469+
};
28470+
path?: never;
28471+
cookie?: never;
28472+
};
28473+
requestBody?: never;
28474+
responses: {
28475+
/** @description Successful Response */
28476+
200: {
28477+
headers: {
28478+
[name: string]: unknown;
28479+
};
28480+
content: {
28481+
"application/json": components["schemas"]["ExportTaskListResponse"];
28482+
};
28483+
};
28484+
/** @description Request Error */
28485+
"4XX": {
28486+
headers: {
28487+
[name: string]: unknown;
28488+
};
28489+
content: {
28490+
"application/json": components["schemas"]["MessageExceptionModel"];
28491+
};
28492+
};
28493+
/** @description Server Error */
28494+
"5XX": {
28495+
headers: {
28496+
[name: string]: unknown;
28497+
};
28498+
content: {
28499+
"application/json": components["schemas"]["MessageExceptionModel"];
28500+
};
28501+
};
28502+
};
28503+
};
2843828504
create_file_landing_api_file_landings_post: {
2843928505
parameters: {
2844028506
query?: never;

client/src/components/Common/models/exportRecordModel.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export interface ExportRecord {
3838
readonly expirationElapsedTime?: string;
3939
readonly hasExpired: boolean;
4040
readonly errorMessage?: string | null;
41+
readonly objectType?: string;
42+
readonly objectId?: string;
4143
}
4244

4345
export class ExportParamsModel implements ExportParams {
@@ -198,4 +200,12 @@ export class ExportRecordModel implements ExportRecord {
198200
get errorMessage() {
199201
return this._data?.export_metadata?.result_data?.error;
200202
}
203+
204+
get objectType() {
205+
return this._requestMetadata?.object_type;
206+
}
207+
208+
get objectId() {
209+
return this._requestMetadata?.object_id;
210+
}
201211
}

client/src/components/Downloads/RecentDownloads.vue

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,25 @@ import { useRouter } from "vue-router/composables";
44
55
import { useDownloadTracker } from "@/composables/downloadTracker";
66
import type { MonitoringRequest } from "@/composables/persistentProgressMonitor";
7+
import { useRemoteExportTracker } from "@/composables/remoteExportTracker";
78
import { useRoundRobinSelector } from "@/composables/roundRobinSelector";
89
import { DEFAULT_POLL_DELAY } from "@/composables/shortTermStorageMonitor";
910
import { useUserStore } from "@/stores/userStore";
1011
1112
import BreadcrumbHeading from "@/components/Common/BreadcrumbHeading.vue";
1213
import ListHeader from "@/components/Common/ListHeader.vue";
1314
import DownloadItemCard from "@/components/Downloads/DownloadItemCard.vue";
15+
import RemoteExportCard from "@/components/Downloads/RemoteExportCard.vue";
1416
1517
const router = useRouter();
1618
1719
const userStore = useUserStore();
1820
const { downloadMonitoringData, untrackDownloadRequest } = useDownloadTracker();
21+
const { remoteExports, isLoading: remoteExportsLoading } = useRemoteExportTracker();
1922
20-
const isEmpty = computed(() => {
21-
return downloadMonitoringData.value.length === 0;
22-
});
23+
const hasLocalDownloads = computed(() => downloadMonitoringData.value.length > 0);
24+
const hasRemoteExports = computed(() => remoteExports.value.length > 0);
25+
const isEmpty = computed(() => !hasLocalDownloads.value && !hasRemoteExports.value && !remoteExportsLoading.value);
2326
2427
const currentListView = computed(() => userStore.currentListViewPreferences.recentDownloads || "grid");
2528
@@ -57,16 +60,34 @@ const breadcrumbItems = [{ title: "Recent Exports & Downloads", to: "/downloads"
5760
<p v-if="isEmpty">
5861
No recent exports or downloads found. When you start a long-running export or download, it will appear here.
5962
</p>
60-
<div v-else class="d-flex flex-wrap">
61-
<DownloadItemCard
62-
v-for="download in downloadMonitoringData"
63-
:key="download.taskId"
64-
:monitoring-data="download"
65-
:update-task-id="taskIdToUpdate"
66-
:grid-view="currentListView == 'grid'"
67-
@onGoTo="onGoTo"
68-
@onDelete="onDelete"
69-
@onDownload="onDownload" />
63+
64+
<!-- Local Downloads Section -->
65+
<div v-if="hasLocalDownloads" class="mb-4">
66+
<h4 class="mb-3">Downloads</h4>
67+
<div class="d-flex flex-wrap">
68+
<DownloadItemCard
69+
v-for="download in downloadMonitoringData"
70+
:key="download.taskId"
71+
:monitoring-data="download"
72+
:update-task-id="taskIdToUpdate"
73+
:grid-view="currentListView == 'grid'"
74+
@onGoTo="onGoTo"
75+
@onDelete="onDelete"
76+
@onDownload="onDownload" />
77+
</div>
78+
</div>
79+
80+
<!-- Remote Exports Section -->
81+
<div v-if="hasRemoteExports" class="mb-4">
82+
<h4 class="mb-3">Remote Exports</h4>
83+
<div class="d-flex flex-wrap">
84+
<RemoteExportCard
85+
v-for="exportRecord in remoteExports"
86+
:key="exportRecord.id"
87+
:export-record="exportRecord"
88+
:grid-view="currentListView == 'grid'"
89+
@onGoTo="onGoTo" />
90+
</div>
7091
</div>
7192
</div>
7293
</template>
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<script setup lang="ts">
2+
import {
3+
faCheckCircle,
4+
faCloudUploadAlt,
5+
faExclamationTriangle,
6+
faInfoCircle,
7+
faSpinner,
8+
} from "@fortawesome/free-solid-svg-icons";
9+
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
10+
import { BAlert } from "bootstrap-vue";
11+
import { computed } from "vue";
12+
13+
import type { CardAction, CardBadge } from "@/components/Common/GCard.types";
14+
import type { ExportRecord } from "@/components/Common/models/exportRecordModel";
15+
16+
import GCard from "@/components/Common/GCard.vue";
17+
18+
interface Props {
19+
/** The export record */
20+
exportRecord: ExportRecord;
21+
/** Whether to display the card in a grid view */
22+
gridView?: boolean;
23+
}
24+
25+
const props = withDefaults(defineProps<Props>(), {
26+
gridView: false,
27+
});
28+
29+
const emit = defineEmits<{
30+
(e: "onGoTo", to: string): void;
31+
}>();
32+
33+
const objectType = computed(() => {
34+
const type = props.exportRecord.objectType;
35+
switch (type) {
36+
case "history":
37+
return "History";
38+
case "invocation":
39+
return "Workflow Invocation";
40+
default:
41+
return "Object";
42+
}
43+
});
44+
45+
const objectId = computed(() => {
46+
return props.exportRecord.objectId;
47+
});
48+
49+
const title = computed(() => {
50+
return `Export ${objectType.value} to File Source`;
51+
});
52+
53+
const targetUri = computed(() => {
54+
return props.exportRecord.importUri || "Unknown destination";
55+
});
56+
57+
const shortTargetUri = computed(() => {
58+
const uri = targetUri.value;
59+
// Extract just the filename from the URI
60+
const parts = uri.split("/");
61+
return parts[parts.length - 1] || uri;
62+
});
63+
64+
const primaryActions = computed(() => {
65+
const actions: CardAction[] = [];
66+
67+
if (objectId.value) {
68+
actions.push({
69+
id: "go-to-object",
70+
label: `Go to ${objectType.value}`,
71+
icon: faInfoCircle,
72+
title: `View details for ${objectType.value}`,
73+
variant: "outline-primary",
74+
handler: onGoToObject,
75+
});
76+
}
77+
78+
return actions;
79+
});
80+
81+
const badges = computed<CardBadge[]>(() => {
82+
const badges: CardBadge[] = [];
83+
84+
if (props.exportRecord.isPreparing) {
85+
badges.push({
86+
id: "in-progress",
87+
title: "Export is in progress",
88+
label: "In Progress",
89+
variant: "info",
90+
});
91+
} else if (props.exportRecord.isReady) {
92+
badges.push({
93+
id: "completed",
94+
title: "Export completed successfully",
95+
label: "Complete",
96+
variant: "success",
97+
});
98+
} else if (props.exportRecord.hasFailed) {
99+
badges.push({
100+
id: "failed",
101+
label: "Failed",
102+
title: "Export failed",
103+
variant: "danger",
104+
});
105+
}
106+
107+
return badges;
108+
});
109+
110+
function onGoToObject() {
111+
const type = props.exportRecord.objectType;
112+
const id = objectId.value;
113+
if (!id) {
114+
return;
115+
}
116+
117+
switch (type) {
118+
case "history":
119+
emit("onGoTo", `/histories/view?id=${id}`);
120+
break;
121+
case "invocation":
122+
emit("onGoTo", `/workflows/invocations/${id}`);
123+
break;
124+
default:
125+
console.warn(`No specific route defined for object type: ${type}`);
126+
break;
127+
}
128+
}
129+
</script>
130+
131+
<template>
132+
<GCard
133+
:id="exportRecord.id"
134+
class="remote-export-card"
135+
:title="title"
136+
:badges="badges"
137+
:primary-actions="primaryActions"
138+
:update-time="exportRecord.date.toISOString()"
139+
:grid-view="gridView"
140+
:title-icon="{
141+
icon: faCloudUploadAlt,
142+
title: 'Export to File Source',
143+
}">
144+
<template v-slot:description>
145+
<p class="text-muted mb-2"><strong>Destination:</strong> {{ shortTargetUri }}</p>
146+
<p v-if="exportRecord.modelStoreFormat" class="text-muted mb-2">
147+
<strong>Format:</strong> {{ exportRecord.modelStoreFormat }}
148+
</p>
149+
<BAlert v-if="exportRecord.isPreparing" variant="info" show>
150+
<FontAwesomeIcon :icon="faSpinner" spin />
151+
<span>Exporting to remote file source...</span>
152+
</BAlert>
153+
<BAlert v-if="exportRecord.isReady" variant="success" show>
154+
<FontAwesomeIcon :icon="faCheckCircle" />
155+
<span>Export completed successfully to {{ shortTargetUri }}</span>
156+
</BAlert>
157+
<BAlert v-if="exportRecord.hasFailed" variant="danger" show>
158+
<FontAwesomeIcon :icon="faExclamationTriangle" />
159+
<span>Export failed.</span>
160+
<span v-if="exportRecord.errorMessage"> <strong>Error:</strong> {{ exportRecord.errorMessage }} </span>
161+
</BAlert>
162+
</template>
163+
</GCard>
164+
</template>

0 commit comments

Comments
 (0)