Replace tasksubmitter with action-button

I've changed tasksubmitter to accept a "thing" and "action" rather than a single URL. This seems more in keeping with
propertyControl, and eliminates the need for loads of
computed properties.

I've also deleted (rather than just commenting out) chunks of the web app that are no longer in use. These may be
reinstated in the future - but we can get them from git
history. It's less confusing not to have vestigial
code in the repo.

This builds OK but is not, as yet, tested with hardware.
This commit is contained in:
Richard Bowman 2024-02-29 21:44:39 +00:00
parent 612b88d58c
commit cbd8b090ec
22 changed files with 225 additions and 2325 deletions

View file

@ -1,65 +0,0 @@
<template>
<div ref="logContainer" class="log-container uk-margin-left uk-margin-right uk-margin">
<div v-if="log">
<div v-for="(item, index) in log" :key="`log_entry_${index}`">
{{ item.message }}
</div>
</div>
<div v-if="taskStatus == 'error'" class="uk-alert uk-alert-danger">
The task failed due to an error. There may be more information in the log.
</div>
<div v-if="taskStatus == 'cancelled'" class="uk-alert uk-alert-warning">
The task was cancelled.
</div>
<div v-if="taskStatus == 'completed'" class="uk-alert uk-alert-success">
The task completed successfully.
</div>
</div>
</template>
<script>
export default {
name: "ActionLogDisplay",
props: {
taskStatus: {
type: String,
required: true
},
log: {
type: Array,
required: true
}
},
watch: {
log: function() {
this.scrollToBottom();
},
taskStatus: function() {
this.scrollToBottom();
}
},
methods: {
scrollToBottom() {
this.$nextTick(function() {
let viewer = this.$refs.logContainer;
viewer.scrollTop = viewer.scrollHeight;
});
}
}
};
</script>
<style scoped>
.log-container {
position: relative;
overflow-y: auto;
overflow-x: auto;
background-color: white;
color: black;
padding: 0.5em;
border: 1px solid black;
}
</style>

View file

@ -1,161 +0,0 @@
<template>
<div class="progress uk-margin-small">
<div v-if="indeterminateProgressBar" class="indeterminate"></div>
<div v-else class="determinate" :style="barWidthFromProgress"></div>
</div>
</template>
<script>
export default {
name: "ActionProgressBar",
props: {
progress: {
type: Number,
required: false,
default: null
},
taskStatus: {
type: String,
required: true
}
},
computed: {
barWidthFromProgress: function() {
var progress = this.progress <= 100 ? this.progress : 100;
var styleString = `width: ${progress}%`;
return styleString;
},
indeterminateProgressBar: function() {
if (this.taskStatus == "pending") return true;
if ((this.taskStatus == "running") & !this.progress) {
return true;
}
return false;
}
}
};
</script>
<style lang="less" scoped>
@import "../../assets/less/theme.less";
.progress {
position: relative;
height: 5px;
display: block;
width: 100%;
background-color: rgba(180, 180, 180, 0.15);
border-radius: 2px;
background-clip: padding-box;
margin: 0.5rem 0 1rem 0;
overflow: hidden;
}
.progress .determinate {
position: absolute;
background-color: inherit;
top: 0;
bottom: 0;
transition: width 0.3s linear;
}
.progress .indeterminate,
.progress .determinate {
background-color: @global-primary-background;
}
.hook-inverse() {
.progress .indeterminate,
.progress .determinate {
background-color: @inverse-primary-muted-color;
}
}
.progress .indeterminate:before {
content: "";
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395)
infinite;
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
}
.progress .indeterminate:after {
content: "";
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
infinite;
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
infinite;
-webkit-animation-delay: 1.15s;
animation-delay: 1.15s;
}
@-webkit-keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@-webkit-keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}
@keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}
</style>

View file

@ -1,340 +0,0 @@
<template>
<div
v-observe-visibility="visibilityChanged"
class="uk-margin-remove uk-padding-remove"
>
<div v-if="taskStarted" ref="isPollingElement">
<action-progress-bar :progress="progress" :task-status="taskStatus" />
<button
v-if="canTerminate && taskRunning"
type="button"
class="uk-button uk-button-danger uk-margin-remove uk-float-right uk-width-1-1"
@click="terminateTask()"
>
Cancel
</button>
</div>
<div>
<button
type="button"
:hidden="taskStarted"
class="uk-button uk-margin-remove uk-width-1-1"
:class="[buttonPrimary ? 'uk-button-primary' : 'uk-button-default']"
@click="bootstrapTask()"
>
{{ submitLabel }}
</button>
</div>
<div
id="modal-center"
ref="statusModal"
class=""
uk-modal="bg-close: false; esc-close: false; stack: true;"
>
<div
id="status-modal"
class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical"
>
<h2>{{ submitLabel }}</h2>
<action-log-display :log="log" :task-status="taskStatus" />
<div id="progress-and-cancel-row">
<div class="stretchy">
<action-progress-bar
:progress="progress"
:task-status="taskStatus"
/>
</div>
<button
v-if="canTerminate && taskRunning"
type="button"
class="uk-button uk-button-danger not-stretchy"
@click="terminateTask()"
>
Cancel
</button>
<button
v-if="!taskStarted"
type="button"
class="uk-button not-stretchy"
@click="hideModal"
>
Close
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
import UIkit from "uikit";
import actionProgressBar from "./actionProgressBar.vue";
import ActionLogDisplay from "./actionLogDisplay.vue";
export default {
name: "TaskSubmitter",
components: { actionProgressBar, ActionLogDisplay },
props: {
submitUrl: {
type: String,
required: true
},
submitData: {
type: [Object, Array],
required: false,
default: () => ({})
},
pollInterval: {
type: Number,
required: false,
default: 1
},
submitLabel: {
type: String,
required: false,
default: "Submit"
},
canTerminate: {
type: Boolean,
required: false,
default: true
},
requiresConfirmation: {
type: Boolean,
required: false,
default: false
},
confirmationMessage: {
type: String,
required: false,
default: "Start task?"
},
buttonPrimary: {
type: Boolean,
required: false,
default: true
},
submitOnEvent: {
type: String,
required: false,
default: null
},
modalProgress: {
type: Boolean,
required: false,
default: false
}
},
data: function() {
return {
taskUrl: null,
progress: null,
taskStarted: false,
taskRunning: false,
log: [],
taskStatus: ""
};
},
watch: {
progress(newval) {
this.$emit("update:progress", newval);
},
taskStarted(newval) {
this.$emit("update:taskStarted", newval);
},
taskRunning(newval) {
this.$emit("update:taskRunning", newval);
},
log(newval) {
this.$emit("update:log", newval);
},
taskStatus(newval) {
this.$emit("update:taskStatus", newval);
}
},
mounted() {
// Check for already running tasks
if (this.taskStarted != true) {
this.checkExistingTasks();
}
// A global signal listener to perform the action
if (this.submitOnEvent) {
this.$root.$on(this.submitOnEvent, () => {
this.bootstrapTask();
});
}
},
beforeDestroy() {
if (this.submitOnEvent) {
this.$root.$off(this.submitOnEvent);
}
},
methods: {
visibilityChanged(isVisible) {
if (isVisible && this.taskStarted != true) {
this.checkExistingTasks();
}
},
checkExistingTasks: function() {
axios.get(this.submitUrl).then(response => {
for (const task of response.data) {
if (task.status == "pending" || task.status == "running") {
this.taskStarted = true;
this.$emit("taskStarted");
this.startPolling(
task.id,
task.links.find(t => t.rel == "self").href
);
}
}
});
},
bootstrapTask: function() {
// Starts the process of creating a new Actiont ask
if (this.requiresConfirmation) {
this.modalConfirm(this.confirmationMessage).then(
() => {
this.startTask();
},
() => {}
);
} else {
this.startTask();
}
},
async startTask() {
// Starts a new Action task
this.$emit("submit", this.submitData);
// Send a request to start a task
this.taskStarted = true;
this.$emit("taskStarted");
try {
let response = await axios.post(this.submitUrl, this.submitData);
if (this.modalProgress) {
UIkit.modal(this.$refs.statusModal).show();
}
// Start the store polling TaskId for success
response = await this.startPolling(
response.data.id,
response.data.href
);
if (response.status == "completed") {
this.$emit("response", response);
this.$emit("completed", response.output);
} else if (response.status == "cancelled") {
this.$emit("cancelled", response);
this.modalNotify(`The action '${this.submitLabel}' was cancelled.`);
}
} catch (error) {
this.$emit("error", error | Error("Unknown error"));
} finally {
// Reset taskRunning and taskId
this.taskRunning = false;
this.taskStarted = false;
this.$emit("finished");
// Update the form data if we're self-updating
if (this.selfUpdate) {
this.getFormData();
}
}
},
startPolling: function(taskId, taskUrl) {
if (this.taskRunning != true) {
// Starts polling an existing Action task
this.taskUrl = taskUrl;
// Start the store polling TaskId for success
this.taskRunning = true;
this.$emit("taskRunning", taskId);
return this.pollTask(taskId, this.pollInterval);
}
},
pollTask: function(taskId, interval) {
interval = interval * 1000 || 500;
var checkCondition = (resolve, reject) => {
// If the condition is met, we're done!
axios
.get(this.taskUrl, { baseURL: this.$store.getters.baseUri })
.then(response => {
var result = response.data.status;
this.taskStatus = result;
if ((result == "running") | (result == "pending")) {
// If the task is still running, we update progress/log,
// and schedule another poll
this.progress = response.data.progress;
this.log = response.data.log;
// Check again after timeout
setTimeout(checkCondition, interval, resolve, reject);
}
// If task ends with an error
else if (result == "error") {
// Pass the error string back with reject
if (!this.progress) this.progress = 1;
reject(new Error(response.data.output));
}
// If task ends without reporting an error
// (NB this includes cancellation)
else {
resolve(response.data);
}
});
};
return new Promise(checkCondition);
},
hideModal() {
UIkit.modal(this.$refs.statusModal).hide();
},
terminateTask: function() {
axios.delete(this.taskUrl, { baseURL: this.$store.getters.baseUri });
}
}
};
</script>
<style lang="less" scoped>
@import "../../assets/less/theme.less";
#progress-and-cancel-row {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-content: stretch;
align-items: center;
width: 100%;
}
#progress-and-cancel-row .stretchy {
flex-grow: 1;
margin-left: 5px;
margin-right: 5px;
}
#progress-and-cancel-row .not-stretchy {
flex-grow: 0;
margin-left: 5px;
margin-right: 5px;
}
#status-modal .log-container {
height: 10em;
}
</style>