Skip to content

Commit

Permalink
TN-3259 research data export - UI not showing the most current downlo…
Browse files Browse the repository at this point in the history
…ad link (#4339)

Address story:
[TN-3259](https://movember.atlassian.net/browse/TN-3259?atlOrigin=eyJpIjoiM2Q1MzQ2ZjE0YzY1NDBjMDk0OTM2OWMxMWMzMTc3ODAiLCJwIjoiamlyYS1zbGFjay1pbnQifQ)
See also Slack convo
[here](https://cirg.slack.com/archives/C5EU4L7PF/p1694733386101599)
Changes include:
- Whenever an data export is initiated on the UI, the frontend will
cache the celery task URL associated with the export
- Address the scenario where the export is left running in the
background, e.g. as when user switches to a different browser tab - when
data export window receives focus again, e.g. as when the user comes
back from a different browser tab, the frontend will now check if the
download has finished by calling the celery task URL cached from the
previous step and update the download link on UI if it has finished
successfully.
- other change - enhance error checking

---------

Co-authored-by: Amy Chen <clone@cesium.cirg.washington.edu>
  • Loading branch information
achen2401 and Amy Chen authored Sep 19, 2023
1 parent bbd1482 commit d8f2528
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 28 deletions.
159 changes: 141 additions & 18 deletions portal/static/js/src/components/ExportInstruments.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@
:initElementId="getInitElementId()"
:exportUrl="getExportUrl()"
:exportIdentifier="currentStudy"
v-on:initExport="handleInitExport"
v-on:doneExport="handleAfterExport"
v-on:initExportCustomEvent="initExportEvent"></ExportDataLoader>
<!-- display link to the last export -->
<div class="export__history" v-if="hasExportHistory()">
<div class="text-muted prompt" v-text="exportHistoryTitle"></div>
<div v-if="exportHistory">
<a :href="exportHistory.url" target="_blank">
<span v-text="exportHistory.instruments.join(', ')"></span>
<span v-text="(exportHistory.instruments || []).join(', ')"></span>
<span v-text="exportHistory.date"></span>
</a>
</div>
Expand Down Expand Up @@ -98,24 +99,25 @@
subStudyIdentifier: "substudy",
mainStudyInstrumentsList:[],
subStudyInstrumentsList:[],
exportHistory: null
exportHistory: null,
currentTaskUrl: null
}};
},
mixins: [CurrentUser],
mounted: function() {
this.setCurrentMainStudy();
this.initCurrentUser(function() {
this.getInstrumentList();
this.setExportHistory(this.getCacheExportedDataInfo());
this.handleSetExportHistory();
}.bind(this));
},
watch: {
currentStudy: function(newVal, oldVal) {
//watch for when study changes
//reset last exported item link as it is specific to each study
this.setExportHistory(this.getCacheExportedDataInfo());
//reset export error
this.resetExportError();
this.handleSetExportHistory();
//reset export display info
this.resetExportInfoUI();
//reset instrument(s) selected
this.resetInstrumentSelections();
},
Expand Down Expand Up @@ -206,8 +208,11 @@
self.instruments.selected = arrSelected;
});
$("#patientsInstrumentList [name='instrument'], #patientsDownloadTypeList [name='downloadType']").on("click", function() {
//clear pre-existing error
self.resetExportError();
//clear pre-existing export info display
self.resetExportInfoUI();
if (self.hasInstrumentsSelection()) {
$("#patientsDownloadButton").removeAttr("disabled");
}
});
//patientsDownloadTypeList downloadType
$("#patientsDownloadTypeList [name='downloadType']").on("click", function() {
Expand All @@ -218,23 +223,34 @@
}
});
$("#dataDownloadModal").on("show.bs.modal", function () {
self.resetExportInfoUI();
self.setInstrumentsListReady();
self.instruments.selected = [];
self.instruments.dataType = "csv";
self.resetExportError();
self.setInstrumentsListReady();
$(this).find("#patientsInstrumentList label").removeClass("active");
$(this).find("[name='instrument']").prop("checked", false);
});
},
resetExportError: function() {
this.$refs.exportDataLoader.clearExportDataUI();
resetExportInfoUI: function() {
this.$refs.exportDataLoader.clearInProgress();
},
setInProgress: function(inProgress) {
if (!inProgress) {
this.resetExportInfoUI();
return;
}
this.$refs.exportDataLoader.setInProgress(true);
},
initExportEvent: function() {
/*
* custom UI events associated with exporting data
*/
let self = this;
$("#dataDownloadModal").on("hide.bs.modal", function () {
$("#"+self.getInitElementId()).removeAttr("data-export-in-progress");
self.setInProgress(false);
});
$(window).on("focus", function() {
self.handleSetExportHistory();
});
},
setDataType: function (event) {
Expand All @@ -259,21 +275,100 @@
var queryStringInstruments = (this.instruments.selected).map(item => `instrument_id=${item}`).join("&");
return `/api/patient/assessment?${queryStringInstruments}&format=${this.instruments.dataType}`;
},
getDefaultExportObj: function() {
return {
study: this.currentStudy,
date: new Date().toLocaleString(),
instruments: this.instruments.selected || []
}
},
handleInitExport: function(statusUrl) {
if (!statusUrl) return;
// whenever the user initiates an export, we cache the associated celery task URL
this.setCacheTask({
...this.getDefaultExportObj(),
url: statusUrl
});
this.currentTaskUrl = statusUrl;
},
handleAfterExport: function(resultUrl) {
//export is done, save the last export to local storage
this.setCacheExportedDataInfo(resultUrl);
},
getCacheExportTaskKey: function() {
return `export_data_task_${this.getUserId()}_${this.currentStudy}`;
},
removeCacheTaskURL: function() {
localStorage.removeItem(this.getCacheExportTaskKey());
},
setCacheTask: function(taskObj) {
if (!taskObj) return;
localStorage.setItem(this.getCacheExportTaskKey(), JSON.stringify(taskObj));
},
getCacheTask: function() {
const task = localStorage.getItem(this.getCacheExportTaskKey());
if (!task) return null;
let resultJSON = null;
try {
resultJSON = JSON.parse(task);
} catch(e) {
console.log("Unable to parse task JSON ", e);
resultJSON = null;
}
return resultJSON;
},
getFinishedStatusURL: function(url) {
if (!url) return "";
return url.replace("/status", "");
},
getExportDataInfoFromTask: function(callback) {
callback = callback || function() {};
const task = this.getCacheTask();
const self = this;
if (!task) {
callback({data: null});
return;
}
const taskURL = task.url;
if (!taskURL) {
callback({data: null});
return;
}
$.getJSON(taskURL, function(data) {
if (!data) {
callback({data: null});
return;
}
// check the status of the celery task and returns the data if it had been successful
const exportStatus = String(data["state"]).toUpperCase();
callback({
data :
exportStatus === "SUCCESS"?
{
...task,
url: self.getFinishedStatusURL(taskURL)
}:
null
});
// callback({
// data: {
// ...task,
// url: self.getFinishedStatusURL(taskURL)
// }
// })
}).fail(function() {
callback({data: null});
});
},
getCacheExportedDataInfoKey: function() {
//uniquely identified by each user and the study
return `exporDataInfo_${this.getUserId()}_${this.currentStudy}}`;
return `exporDataInfo_${this.getUserId()}_${this.currentStudy}`;
},
setCacheExportedDataInfo: function(resultUrl) {
if (!resultUrl) return false;
if (!this.hasInstrumentsSelection()) return;
var o = {
study: this.currentStudy,
date: new Date().toLocaleString(),
instruments: this.instruments.selected,
...this.getDefaultExportObj(),
url: resultUrl
};
localStorage.setItem(this.getCacheExportedDataInfoKey(), JSON.stringify(o));
Expand All @@ -284,14 +379,42 @@
if (!cachedItem) {
return null;
}
return JSON.parse(cachedItem);
let resultJSON = null;
try {
resultJSON = JSON.parse(cachedItem);
} catch(e) {
console.log("Unable to parse cached data export info ", e);
resultJSON = null;
}
return resultJSON;
},
hasExportHistory: function() {
return this.exportHistory || this.getCacheExportedDataInfo();
},
setExportHistory: function(o) {
this.exportHistory = o;
},
handleSetExportHistory: function() {
const self = this;
this.getExportDataInfoFromTask(function(data) {
if (data && data.data) {
this.setExportHistory(data.data);
const task = this.getCacheTask();
// console.log("current task URL ", self.getFinishedStatusURL(self.currentTaskUrl));
// console.log("cached task URL ", self.getFinishedStatusURL(task.url));
if (task &&
task.url &&
self.getFinishedStatusURL(task.url) === self.getFinishedStatusURL(self.currentTaskUrl)) {
this.setInProgress(false);
}
return;
}
const cachedDataInfo = this.getCacheExportedDataInfo();
if (cachedDataInfo) {
this.setExportHistory(cachedDataInfo);
}
}.bind(this));
}
}
};
</script>
28 changes: 18 additions & 10 deletions portal/static/js/src/components/asyncExportDataLoader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,15 @@
clearTimeElapsed: function() {
this.exportTimeElapsed = 0;
},
onBeforeExportData: function() {
clearInProgress: function() {
this.updateExportProgress("", "");
this.clearExportDataTimeoutID();
this.clearTimeElapsed();
this.clearExportDataUI();
this.setInProgress(false);
},
onBeforeExportData: function() {
this.clearInProgress();
this.setInProgress(true);
$("#" + this.initElementId).attr("disabled", true);
$(".export__display-container").addClass("active");
Expand All @@ -103,7 +107,6 @@
},
onAfterExportData: function(options) {
options = options || {};
let delay = options.delay||5000;
$("#" + this.initElementId).attr("disabled", false);
$(".export__status").removeClass("active");
this.setInProgress();
Expand All @@ -129,6 +132,8 @@
url: exportDataUrl,
success: function(data, status, request) {
let statusUrl= request.getResponseHeader("Location");
// console.log("status URL ", statusUrl)
self.$emit("initExport", statusUrl);
self.updateExportProgress(statusUrl, function(data) {
self.onAfterExportData(data);
});
Expand Down Expand Up @@ -165,7 +170,7 @@
let self = this;
let waitTime = 3000;
// send GET request to status URL
let rqId = $.getJSON(statusUrl, function(data) {
$.getJSON(statusUrl, function(data) {
if (!data) {
callback({error: true});
return;
Expand All @@ -183,12 +188,12 @@
percent = parseInt(data['current'] * 100 / data['total']) + "%";
} else {
percent = " -- %";
//allow maximum allowed elapsed time of pending status and no progress percentage returned,
//if still no progress returned, then return error and display message
if (exportStatus === "PENDING" && self.exportTimeElapsed > self.maximumPendingTime) {
callback({error: true, message: "Processing job not responding. Please try again.", delay: 10000});
return;
}
}
//allow maximum allowed elapsed time of pending status and no progress percentage returned,
//if still no progress returned, then return error and display message
if (exportStatus === "PENDING" && self.exportTimeElapsed > self.maximumPendingTime) {
callback({error: true, message: "Processing job not responding. Please try again.", delay: 30000});
return;
}
//update status and percentage displays
self.updateProgressDisplay(exportStatus, percent, true);
Expand All @@ -199,6 +204,9 @@
setTimeout(function() {
window.location.assign(resultUrl);
}, 50); //wait a bit before retrieving results
} else {
callback({error: true, message: "Unknown status return from the processing job. Status: ", exportStatus});
return;
}
self.updateProgressDisplay(data["state"], "");
setTimeout(function() {
Expand All @@ -214,7 +222,7 @@
(self.arrExportDataTimeoutID).push(self.exportDataTimeoutID);
}
}).fail(function() {
callback({error: true});
callback({error: true, message: "The processing job has failed."});
});
}
}
Expand Down

0 comments on commit d8f2528

Please sign in to comment.