Action button uses mixins for invoking and polling actions

This commit is contained in:
Julian Stirling 2025-11-09 16:08:55 +00:00
parent 4910bef717
commit dd0a57520b
2 changed files with 118 additions and 102 deletions

View file

@ -47,8 +47,6 @@
</template> </template>
<script> <script>
import axios from "axios";
import ActionProgressBar from "./actionProgressBar.vue"; import ActionProgressBar from "./actionProgressBar.vue";
import ActionStatusModal from "./actionStatusModal.vue"; import ActionStatusModal from "./actionStatusModal.vue";
@ -165,7 +163,7 @@ export default {
n = Math.trunc(n) || 0; n = Math.trunc(n) || 0;
// Allow negative indexing from the end // Allow negative indexing from the end
if (n < 0) n += this.length; if (n < 0) n += this.length;
// OOB access is guaranteed to return undefined // Out of bounds access is guaranteed to return undefined
if (n < 0 || n >= this.length) return undefined; if (n < 0 || n >= this.length) return undefined;
// Otherwise, this is just normal property access // Otherwise, this is just normal property access
return this[n]; return this[n];
@ -215,13 +213,9 @@ export default {
* *
*/ */
async checkExistingTasks() { async checkExistingTasks() {
let response; let response = await this.findOngoingAction(this.thing, this.action);
try { // Exit if response is null, due to an error.
response = await axios.get(this.submitUrl); if (response == null) return;
} catch (error) {
console.warn("checkExistingTasks: request failed", error);
return;
}
// Check for a task that is ongoing. // Check for a task that is ongoing.
// We can't handle multiple tasks ongoing, so this picks the first. // We can't handle multiple tasks ongoing, so this picks the first.
const ongoingTask = response.data.find((t) => ["pending", "running"].includes(t.status)); const ongoingTask = response.data.find((t) => ["pending", "running"].includes(t.status));
@ -231,13 +225,7 @@ export default {
this.$emit("taskStarted"); this.$emit("taskStarted");
// Find its URL // Find its URL
const taskUrl = ongoingTask.links.find((t) => t.rel == "self").href; const taskUrl = ongoingTask.links.find((t) => t.rel == "self").href;
try { this.startPollingTask(ongoingTask.id, taskUrl);
await this.pollOngoingTask(ongoingTask.id, taskUrl);
} catch (error) {
this.$emit("error", error);
} finally {
this.onTaskEnd();
}
} }
}, },
@ -257,108 +245,106 @@ export default {
async startTask() { async startTask() {
// Starts a new Action task // Starts a new Action task
this.$emit("submit", this.submitData); this.$emit("submit", this.submitData);
// Send a request to start a task // Send a request to start a task
this.taskStarted = true; this.taskStarted = true;
this.$emit("taskStarted"); this.$emit("taskStarted");
let response;
try { try {
let response = await axios.post(this.submitUrl, this.submitData); response = await this.invokeAction(
if (this.modalProgress) { this.thing,
this.$refs.statusModal.show(); this.action,
} this.submitData,
await this.pollOngoingTask(response.data.id, response.data.href); false, // Stop invokeAction handling the error.
);
} catch (error) { } catch (error) {
this.$emit("error", error); this.$emit("error", error);
} finally {
this.onTaskEnd(); this.onTaskEnd();
return;
} }
if (this.modalProgress) {
this.$refs.statusModal.show();
}
// This just starts the polling. No need to await it.
this.startPollingTask(response.data.id, response.data.href);
}, },
async pollOngoingTask(taskId, taskUrl) { async startPollingTask(taskId, taskUrl) {
// Return if taskRunning already set.
if (this.taskRunning) return;
// Starts polling an existing Action task
this.taskUrl = taskUrl;
// Start the store polling TaskId for success // Start the store polling TaskId for success
const response = await this.startPolling(taskId, taskUrl); this.taskRunning = true;
if (response.status == "completed") { this.$emit("taskRunning", taskId);
this.$emit("response", response); this.pollUntilComplete(
this.$emit("completed", response.output); taskUrl,
} else if (response.status == "cancelled") { this.onPollingResponse,
this.$emit("cancelled", response); this.onTaskEnd, // Method to run after task (even if error)
this.modalNotify(`The action '${this.submitLabel}' was cancelled.`); 500, // Interval
} false, // Don't handle errors,
);
}, },
onTaskEnd: function () { onTaskEnd: function (response) {
// Reset taskRunning and taskId if (response) {
this.taskStatus = response.data.status;
this.log = response.data.log;
if (response.data.status == "completed") {
this.$emit("response", response.data);
this.$emit("completed", response.data.output);
} else if (response.data.status == "cancelled") {
this.$emit("cancelled", response.data);
this.modalNotify(`The action '${this.submitLabel}' was cancelled.`);
}
}
this.taskUrl = null;
this.taskRunning = false; this.taskRunning = false;
this.taskStarted = false; this.taskStarted = false;
this.$emit("finished"); this.$emit("finished");
}, },
startPolling: function (taskId, taskUrl) { onPollingResponse(response) {
if (this.taskRunning != true) { var result = response.data.status;
// Starts polling an existing Action task this.taskStatus = result;
this.taskUrl = taskUrl; if ((result == "running") | (result == "pending")) {
// Start the store polling TaskId for success // If the task is still running, we update progress/log,
this.taskRunning = true; // and schedule another poll
this.$emit("taskRunning", taskId); this.progress = response.data.progress;
return this.pollTask(taskId, this.pollInterval); this.log = response.data.log;
}
// If task ends with an error
else if (result == "error") {
this.handleErrorResponse(response);
} }
}, },
pollTask: function (taskId, interval) { handleErrorResponse(response) {
interval = interval * 1000 || 500; // Pass the error string back with reject
if (!this.progress) this.progress = 1;
var checkCondition = (resolve, reject) => { // Test whether the log is empty or the most recent message is not from an error
// If the condition is met, we're done! // If so, return a default message
axios.get(this.taskUrl, { baseURL: this.$store.getters.baseUri }).then((response) => { if (
var result = response.data.status; (response.data.log.length == 0) |
this.taskStatus = result; (response.data.log.from_index(-1).levelname != "ERROR")
if ((result == "running") | (result == "pending")) { ) {
// If the task is still running, we update progress/log, var message = "Unexpected error, please check the logs";
// and schedule another poll }
this.progress = response.data.progress; // As LabThings Actions add the message from any raised exception to the log, the
this.log = response.data.log; // last message in the log is the message from the Exception.
// Check again after timeout // If the Exception was raised with no message, use a default.
setTimeout(checkCondition, interval, resolve, reject); else {
} message =
// If task ends with an error response.data.log.from_index(-1).message || "Unexpected error, please check the logs";
else if (result == "error") { }
// Pass the error string back with reject // Raise an Error with the chosen message
if (!this.progress) this.progress = 1; throw new Error(message);
// Test whether the log is empty or the most recent message is not from an error
// If so, return a default message
if (
(response.data.log.length == 0) |
(response.data.log.from_index(-1).levelname != "ERROR")
) {
var message = "Unexpected error, please check the logs";
}
// As LabThings Actions add the message from any raised exception to the log, the
// last message in the log is the message from the Exception.
// If the Exception was raised with no message, use a default.
else {
message =
response.data.log.from_index(-1).message ||
"Unexpected error, please check the logs";
}
// Raise an Error with the chosen message
reject(new Error(message));
}
// If task ends without reporting an error
// (Note: this includes cancellation)
else {
resolve(response.data);
}
});
};
return new Promise(checkCondition);
}, },
terminateTask: function () { terminateTask: function () {
axios.delete(this.taskUrl, { baseURL: this.$store.getters.baseUri }); if (this.taskUrl) {
this.$root.$emit("modalClosed"); this.terminateAction(this.taskUrl);
}
}, },
}, },
}; };

View file

@ -64,19 +64,31 @@ export default {
} }
await axios.put(url, value); await axios.put(url, value);
}, },
async invokeAction(thing, action, data) { async invokeAction(thing, action, data, handleErrors = true) {
let url = this.$store.getters["wot/thingActionUrl"](thing, action, "invokeaction", false); let url = this.thingActionUrl(thing, action);
try { try {
let response = await axios.post(url, data); let response = await axios.post(url, data);
return response; return response;
} catch (error) { } catch (error) {
this.modalError(error); if (handleErrors) {
return undefined; this.modalError(error);
return undefined;
} else {
throw error;
}
} }
}, },
async pollUntilComplete(taskUrl, ongoingMethod, finalMethod, interval = 500) { async pollUntilComplete(
taskUrl,
ongoingMethod,
finalMethod,
interval = 500,
modalErrors = true,
) {
let response;
let finalMethodCalled = false;
try { try {
const response = await axios.get(taskUrl, { baseURL: this.$store.getters.baseUri }); response = await axios.get(taskUrl, { baseURL: this.$store.getters.baseUri });
const result = response.data.status; const result = response.data.status;
if ((result == "running") | (result == "pending")) { if ((result == "running") | (result == "pending")) {
@ -87,15 +99,33 @@ export default {
} else { } else {
clearTimeout(this.pollTimers[taskUrl]); clearTimeout(this.pollTimers[taskUrl]);
delete this.pollTimers[taskUrl]; delete this.pollTimers[taskUrl];
finalMethodCalled = true;
finalMethod?.(response); finalMethod?.(response);
} }
} catch (error) { } catch (error) {
this.$emit("error", error);
if (modalErrors) {
this.modalError(error);
}
clearTimeout(this.pollTimers[taskUrl]); clearTimeout(this.pollTimers[taskUrl]);
delete this.pollTimers[taskUrl]; delete this.pollTimers[taskUrl];
this.modalError(error); if (!finalMethodCalled) {
finalMethod?.(response);
}
}
},
terminateAction(taskUrl) {
axios.delete(taskUrl, { baseURL: this.$store.getters.baseUri });
},
async findOngoingAction(thing, action) {
let url = this.thingActionUrl(thing, action);
try {
return await axios.get(url);
} catch (error) {
console.warn("checkExistingTasks: request failed", error);
return null;
} }
}, },
thingActionUrl(thing, action, allowUndefined = false) { thingActionUrl(thing, action, allowUndefined = false) {
let url = this.$store.getters["wot/thingActionUrl"]( let url = this.$store.getters["wot/thingActionUrl"](
thing, thing,