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:
parent
612b88d58c
commit
cbd8b090ec
22 changed files with 225 additions and 2325 deletions
350
webapp/src/components/labThingsComponents/actionButton.vue
Normal file
350
webapp/src/components/labThingsComponents/actionButton.vue
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
<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: "ActionButton",
|
||||
components: { ActionProgressBar, ActionLogDisplay },
|
||||
|
||||
props: {
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
thing: {
|
||||
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: ""
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
submitUrl() {
|
||||
return this.thingActionUrl(this.thing, this.action);
|
||||
}
|
||||
},
|
||||
|
||||
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>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<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>
|
||||
161
webapp/src/components/labThingsComponents/actionProgressBar.vue
Normal file
161
webapp/src/components/labThingsComponents/actionProgressBar.vue
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<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>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<label v-if="dataType == 'number'" class="uk-form-label">{{ label }}
|
||||
<label v-if="dataType == 'number'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-model="value"
|
||||
|
|
@ -30,7 +31,8 @@
|
|||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
<label v-if="dataType == 'number_array'" class="uk-form-label">{{ label }}
|
||||
<label v-if="dataType == 'number_array'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-for="i in valueLength"
|
||||
|
|
@ -47,7 +49,8 @@
|
|||
</a>
|
||||
</div>
|
||||
</label>
|
||||
<label v-if="dataType == 'number_object'" class="uk-form-label">{{ label }}
|
||||
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-for="(_v, key) in value"
|
||||
|
|
@ -64,7 +67,8 @@
|
|||
</a>
|
||||
</div>
|
||||
</label>
|
||||
<label v-if="dataType == 'other'" class="uk-form-label">{{ label }}
|
||||
<label v-if="dataType == 'other'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
:value="value"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue