Merge branch 'action-tab-2' into 'v3'
Split up slide scan tab and create a generic action tab (attempt 2) See merge request openflexure/openflexure-microscope-server!536
This commit is contained in:
commit
65ac551688
5 changed files with 465 additions and 205 deletions
|
|
@ -50,6 +50,18 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
// forceId and forceUrl can be used if it is known that when this button is mounted
|
||||||
|
// the action should be running.
|
||||||
|
forceId: {
|
||||||
|
type: [String, null],
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
forceUrl: {
|
||||||
|
type: [String, null],
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
submitData: {
|
submitData: {
|
||||||
type: [Object, Array],
|
type: [Object, Array],
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -197,17 +209,22 @@ export default {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for already running tasks
|
if (this.forceId && this.forceUrl) {
|
||||||
if (this.taskStarted != true) {
|
this.taskStarted = true;
|
||||||
this.checkExistingTasks();
|
this.startPollingTask(this.forceId, this.forceUrl);
|
||||||
}
|
} else {
|
||||||
// A global signal listener to perform the action
|
// Check for already running tasks
|
||||||
if (this.submitOnEvent) {
|
if (this.taskStarted != true) {
|
||||||
eventBus.on(this.submitOnEvent, () => {
|
this.checkExistingTasks();
|
||||||
if (this.isDisabled) return;
|
}
|
||||||
// Bootstrap task if button is not disabled.
|
// A global signal listener to perform the action
|
||||||
this.bootstrapTask();
|
if (this.submitOnEvent) {
|
||||||
});
|
eventBus.on(this.submitOnEvent, () => {
|
||||||
|
if (this.isDisabled) return;
|
||||||
|
// Bootstrap task if button is not disabled.
|
||||||
|
this.bootstrapTask();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -240,18 +257,13 @@ export default {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async checkExistingTasks() {
|
async checkExistingTasks() {
|
||||||
let response = await this.findOngoingActions(this.thing, this.action);
|
const ongoingTask = await this.getOngingAction(this.thing, this.action);
|
||||||
// Exit if response is null, due to an error.
|
|
||||||
if (response == null) return;
|
|
||||||
// Check for a task that is ongoing.
|
|
||||||
// We can't handle multiple tasks ongoing, so this picks the first.
|
|
||||||
const ongoingTask = response.data.find((t) => ["pending", "running"].includes(t.status));
|
|
||||||
if (ongoingTask) {
|
if (ongoingTask) {
|
||||||
// There is a started task
|
// There is a started task
|
||||||
this.taskStarted = true;
|
this.taskStarted = true;
|
||||||
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;
|
||||||
|
this.$emit("taskStarted", ongoingTask.id, taskUrl);
|
||||||
this.startPollingTask(ongoingTask.id, taskUrl);
|
this.startPollingTask(ongoingTask.id, taskUrl);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -275,7 +287,6 @@ export default {
|
||||||
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");
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await this.invokeAction(
|
response = await this.invokeAction(
|
||||||
|
|
@ -289,6 +300,9 @@ export default {
|
||||||
this.onTaskEnd();
|
this.onTaskEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Wait until after the task is acknowledged before confirming to parent
|
||||||
|
// component that the task is started.
|
||||||
|
this.$emit("taskStarted", response.data.id, response.data.href);
|
||||||
if (this.modalProgress) {
|
if (this.modalProgress) {
|
||||||
this.$refs.statusModal.show();
|
this.$refs.statusModal.show();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
224
webapp/src/components/tabContentComponents/actionTab.vue
Normal file
224
webapp/src/components/tabContentComponents/actionTab.vue
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
<!-- A tab that changes its sidebar when an action is started.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is a generic component used to build tabs that need to switch view when an action is started,
|
||||||
|
such as the Slide Scan tab.
|
||||||
|
|
||||||
|
The task runs in 3 modes:
|
||||||
|
|
||||||
|
* **Unstarted** - No task has been started.
|
||||||
|
* **Running** - The task is running.
|
||||||
|
* **Completed** - The task has completed.
|
||||||
|
|
||||||
|
## HTML Slots and Template
|
||||||
|
|
||||||
|
The template has 3 slots.
|
||||||
|
|
||||||
|
* The main slot is the main view on the right hand side of the tab. To display the microscope stream
|
||||||
|
use `<streamDisplay />` here
|
||||||
|
* **controls** - A named slot for the controls that are shown in the sidebar in **Unstarted** mode.
|
||||||
|
* **task-info** - A slot for any information or controls to show at the bottom of the sidebar in
|
||||||
|
**Running** or **Completed** mode.
|
||||||
|
|
||||||
|
In **Running** or **Completed** modes the sidebar automatically shows:
|
||||||
|
|
||||||
|
* **A tile** if `taskInfoTitle` prop is set.
|
||||||
|
* **A mini stream** if `taskInfoStream` prop is `true`
|
||||||
|
* **A log view**
|
||||||
|
* **A cancel button** or a **Close** button depending on if the mode is **Running** or **Completed**
|
||||||
|
|
||||||
|
## Changing mode
|
||||||
|
|
||||||
|
To notify the tab that the action running the `taskId` and `taskUrl` props should be set. These values
|
||||||
|
are the data send from the `taskStarted` event of `actionButton`.
|
||||||
|
|
||||||
|
**Note:** This component will check whether the task has been started externally (for example, by
|
||||||
|
another window). A `actionStartedExternally` event with `taskId` and `taskUrl` data will be emitted in
|
||||||
|
this case. The tab does not mutate its own props so it the parent component should subscribe to the
|
||||||
|
event and set the props accordingly.
|
||||||
|
|
||||||
|
When the task completes a `completed` event is emitted. No action is needed on this event, it is emitted
|
||||||
|
so that the parent component can update any other data or UI elements if needed.
|
||||||
|
|
||||||
|
When the close button is clicked a `closeTask` event is emitted. It is the responsibility of the parent
|
||||||
|
component to set the `taskId` and `taskUrl` props back to `null`, returning the tab to **Unstarted**
|
||||||
|
mode.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div ref="actionTab" uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||||
|
<div class="control-component uk-padding-small">
|
||||||
|
<div v-if="!taskStarted" class="uk-padding-small">
|
||||||
|
<slot name="controls"></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="taskStarted">
|
||||||
|
<h2 v-if="taskInfoTitle" style="text-align: center">{{ taskInfoTitle }}</h2>
|
||||||
|
<mini-stream-display v-if="taskInfoStream" />
|
||||||
|
<action-log-display id="log-display" :log="log" :task-status="taskStatus" />
|
||||||
|
<div class="uk-width-1-1 uk-flex uk-flex-center">
|
||||||
|
<div class="uk-width-2-3">
|
||||||
|
<action-button
|
||||||
|
v-if="taskRunning"
|
||||||
|
:thing="thing"
|
||||||
|
:action="action"
|
||||||
|
:force-id="taskId"
|
||||||
|
:force-url="taskUrl"
|
||||||
|
submit-label=""
|
||||||
|
:can-terminate="true"
|
||||||
|
@completed="$emit('completed')"
|
||||||
|
@update:task-status="taskStatus = $event"
|
||||||
|
@update:progress="progress = $event"
|
||||||
|
@update:log="log = $event"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="!taskRunning"
|
||||||
|
type="button"
|
||||||
|
class="uk-button uk-width-1-1 uk-position-relative"
|
||||||
|
@click="closeTask"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot name="task-info"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="main-view uk-width-expand uk-height-1-1">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import actionLogDisplay from "../labThingsComponents/actionLogDisplay.vue";
|
||||||
|
import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
||||||
|
import ActionButton from "../labThingsComponents/actionButton.vue";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ActionTab",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
actionLogDisplay,
|
||||||
|
MiniStreamDisplay,
|
||||||
|
ActionButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
thing: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
taskId: {
|
||||||
|
type: [String, null],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
taskUrl: {
|
||||||
|
type: [String, null],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
taskInfoTitle: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
taskInfoStream: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ["closeTask", "completed", "actionStartedExternally"],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
taskStatus: "pending",
|
||||||
|
progress: null,
|
||||||
|
log: [],
|
||||||
|
pollTimer: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
taskStarted() {
|
||||||
|
return this.taskId && this.taskUrl;
|
||||||
|
},
|
||||||
|
taskRunning() {
|
||||||
|
return (this.taskStatus == "running") | (this.taskStatus == "pending");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
useIntersectionObserver(
|
||||||
|
this.$refs.actionTab,
|
||||||
|
([{ isIntersecting }]) => {
|
||||||
|
this.visibilityChanged(isIntersecting);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.0, // Adjust as needed
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Reset the data needed for actions to be polled.
|
||||||
|
*/
|
||||||
|
resetData() {
|
||||||
|
this.taskStatus = "pending";
|
||||||
|
this.log = [];
|
||||||
|
},
|
||||||
|
closeTask() {
|
||||||
|
// Reset task status to pending so that it is re-read next time an action starts.
|
||||||
|
this.resetData();
|
||||||
|
this.$emit("closeTask");
|
||||||
|
},
|
||||||
|
visibilityChanged(isVisible) {
|
||||||
|
this.isVisible = isVisible;
|
||||||
|
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.startTimer();
|
||||||
|
} else {
|
||||||
|
this.stopTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startTimer() {
|
||||||
|
if (this.pollTimer) return;
|
||||||
|
|
||||||
|
this.pollTimer = setInterval(() => {
|
||||||
|
this.checkIfStartedExternally();
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
stopTimer() {
|
||||||
|
if (!this.pollTimer) return;
|
||||||
|
|
||||||
|
clearInterval(this.pollTimer);
|
||||||
|
this.pollTimer = null;
|
||||||
|
},
|
||||||
|
async checkIfStartedExternally() {
|
||||||
|
if (!this.taskStarted | !this.taskRunning) {
|
||||||
|
const ongoingTask = await this.getOngingAction(this.thing, this.action);
|
||||||
|
if (ongoingTask) {
|
||||||
|
this.resetData();
|
||||||
|
const taskUrl = ongoingTask.links.find((t) => t.rel == "self").href;
|
||||||
|
this.$emit("actionStartedExternally", ongoingTask.id, taskUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.control-component {
|
||||||
|
width: 33%;
|
||||||
|
}
|
||||||
|
#log-display {
|
||||||
|
height: 20em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
<template>
|
||||||
|
<!-- Workflow Selection Dropdown -->
|
||||||
|
<div ref="slideScanControls" class="uk-margin">
|
||||||
|
<label class="uk-form-label">Workflow</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
class="uk-select uk-form-small"
|
||||||
|
:value="workflowName"
|
||||||
|
@change="setWorkflow($event.target.value)"
|
||||||
|
>
|
||||||
|
<option v-for="(label, name) in workflowOptions" :key="name" :value="name">
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<server-specified-interface :elements="workflowSettings" @request-update="readSettings" />
|
||||||
|
<ul uk-accordion="multiple: true">
|
||||||
|
<li class="uk-open">
|
||||||
|
<a class="uk-accordion-title" href="#">Stitching Settings</a>
|
||||||
|
<div class="uk-accordion-content">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<propertyControl
|
||||||
|
thing-name="smart_scan"
|
||||||
|
property-name="stitch_automatically"
|
||||||
|
label="Automatically Stitch Images Together"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<propertyControl
|
||||||
|
thing-name="smart_scan"
|
||||||
|
property-name="stitch_tiff"
|
||||||
|
label="When Stitching, Produce a Pyramidal TIFF"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import propertyControl from "@/components/labThingsComponents/propertyControl.vue";
|
||||||
|
import ServerSpecifiedInterface from "@/components/labThingsComponents/serverSpecifiedInterface.vue";
|
||||||
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SlideScanControls",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
propertyControl,
|
||||||
|
ServerSpecifiedInterface,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
workflowName: undefined,
|
||||||
|
workflowSettings: [],
|
||||||
|
workflowOptions: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
this.readSettings();
|
||||||
|
this.workflowOptions = await this.readThingProperty(
|
||||||
|
"smart_scan",
|
||||||
|
"workflow_display_names",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// sets visibilityChanged to true or false, which can then update settings if needed
|
||||||
|
useIntersectionObserver(
|
||||||
|
this.$refs.slideScanControls,
|
||||||
|
([{ isIntersecting }]) => {
|
||||||
|
this.visibilityChanged(isIntersecting);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
visibilityChanged(isVisible) {
|
||||||
|
if (isVisible) {
|
||||||
|
this.readSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async readSettings() {
|
||||||
|
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name");
|
||||||
|
|
||||||
|
if (!this.workflowName) {
|
||||||
|
console.warn("Could not read workflow_name, using default");
|
||||||
|
this.workflowName = "histo_scan_workflow";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.workflowName) {
|
||||||
|
this.ready = await this.readThingProperty(this.workflowName, "ready", true);
|
||||||
|
this.workflowSettings =
|
||||||
|
(await this.getThingEndpoint(this.workflowName, "settings_ui")) || [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setWorkflow(name) {
|
||||||
|
try {
|
||||||
|
this.workflowName = name;
|
||||||
|
|
||||||
|
await this.writeThingProperty("smart_scan", "workflow_name", name);
|
||||||
|
|
||||||
|
// refresh UI
|
||||||
|
await this.readSettings();
|
||||||
|
} catch (err) {
|
||||||
|
this.modalError(err);
|
||||||
|
|
||||||
|
// revert if server rejected
|
||||||
|
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name", true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -1,87 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
<actionTab
|
||||||
<div class="control-component uk-padding-small">
|
thing="smart_scan"
|
||||||
<div v-show="!scanning" ref="slideScanView" class="uk-padding-small">
|
action="sample_scan"
|
||||||
<!-- Workflow Selection Dropdown -->
|
:task-id="taskId"
|
||||||
<div class="uk-margin">
|
:task-url="taskUrl"
|
||||||
<label class="uk-form-label">Workflow</label>
|
:task-info-title="infoPaneTitle"
|
||||||
|
:task-info-stream="displayImageOnRight"
|
||||||
<select
|
@completed="onScanCompleted"
|
||||||
class="uk-select uk-form-small"
|
@close-task="closeTask"
|
||||||
:value="workflowName"
|
@action-started-externally="startScanning"
|
||||||
@change="setWorkflow($event.target.value)"
|
>
|
||||||
>
|
<!-- MainView -->
|
||||||
<option v-for="(label, name) in workflowOptions" :key="name" :value="name">
|
<img
|
||||||
{{ label }}
|
v-if="displayImageOnRight"
|
||||||
</option>
|
id="last-stitched-image"
|
||||||
</select>
|
class="image-fit"
|
||||||
</div>
|
:src="lastStitchedImage"
|
||||||
<server-specified-interface :elements="workflowSettings" @request-update="readSettings" />
|
/>
|
||||||
<ul uk-accordion="multiple: true">
|
<streamDisplay v-else />
|
||||||
<li class="uk-open">
|
<template #controls>
|
||||||
<a class="uk-accordion-title" href="#">Stitching Settings</a>
|
<slideScanControls />
|
||||||
<div class="uk-accordion-content">
|
<label class="uk-form-label" for="form-stacked-text">Sample ID</label>
|
||||||
<div class="uk-margin">
|
<div class="uk-form-controls">
|
||||||
<propertyControl
|
<input v-model="scan_name" class="uk-input uk-form-small" type="text" name="Scan Name" />
|
||||||
thing-name="smart_scan"
|
|
||||||
property-name="stitch_automatically"
|
|
||||||
label="Automatically Stitch Images Together"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<propertyControl
|
|
||||||
thing-name="smart_scan"
|
|
||||||
property-name="stitch_tiff"
|
|
||||||
label="When Stitching, Produce a Pyramidal TIFF"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<label class="uk-form-label" for="form-stacked-text">Sample ID</label>
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<input v-model="scan_name" class="uk-input uk-form-small" type="text" name="Scan Name" />
|
|
||||||
</div>
|
|
||||||
<div class="uk-margin">
|
|
||||||
<action-button
|
|
||||||
ref="smartScanButton"
|
|
||||||
thing="smart_scan"
|
|
||||||
action="sample_scan"
|
|
||||||
:submit-data="{ scan_name: scan_name }"
|
|
||||||
submit-label="Start Smart Scan"
|
|
||||||
:can-terminate="true"
|
|
||||||
@task-started="startScanning"
|
|
||||||
@update:task-status="taskStatus = $event"
|
|
||||||
@update:progress="progress = $event"
|
|
||||||
@update:log="log = $event"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-show="scanning">
|
<div class="uk-margin">
|
||||||
<h2 v-if="displayImageOnRight" style="text-align: center">Live stitching preview</h2>
|
<action-button
|
||||||
<mini-stream-display v-if="displayImageOnRight" />
|
ref="smartScanButton"
|
||||||
<action-log-display id="log-display" :log="log" :task-status="taskStatus" />
|
thing="smart_scan"
|
||||||
<action-progress-bar :progress="progress" :task-status="taskStatus" />
|
action="sample_scan"
|
||||||
<button
|
:submit-data="{ scan_name: scan_name }"
|
||||||
v-if="cancellable"
|
submit-label="Start Smart Scan"
|
||||||
type="button"
|
@task-started="startScanning"
|
||||||
class="uk-button uk-button-danger uk-width-1-1"
|
/>
|
||||||
@click="$refs.smartScanButton.terminateTask()"
|
</div>
|
||||||
>
|
</template>
|
||||||
Cancel
|
<template #task-info>
|
||||||
</button>
|
<div class="uk-width-1-1 uk-flex uk-flex-center">
|
||||||
<div v-if="!cancellable" class="uk-margin uk-grid-small uk-child-width-expand" uk-grid>
|
<div class="uk-width-2-3">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="uk-button"
|
|
||||||
@click="
|
|
||||||
scanning = false;
|
|
||||||
lastStitchedImage = null;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
<action-button
|
<action-button
|
||||||
|
v-if="scanComplete"
|
||||||
thing="smart_scan"
|
thing="smart_scan"
|
||||||
action="download_zip"
|
action="download_zip"
|
||||||
submit-label="Download ZIP"
|
submit-label="Download ZIP"
|
||||||
|
|
@ -93,114 +51,52 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 v-if="scanning">Scan ID: {{ lastScanName }}</h3>
|
<h3 v-if="scanning" class="uk-margin-left">Scan ID: {{ lastScanName }}</h3>
|
||||||
</div>
|
</template>
|
||||||
<div class="view-image uk-width-expand uk-height-1-1">
|
</actionTab>
|
||||||
<img
|
|
||||||
v-if="displayImageOnRight"
|
|
||||||
id="last-stitched-image"
|
|
||||||
class="image-fit"
|
|
||||||
:src="lastStitchedImage"
|
|
||||||
/>
|
|
||||||
<streamDisplay v-else />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import actionTab from "./actionTab.vue";
|
||||||
|
import slideScanControls from "./slideScanComponents/slideScanControls.vue";
|
||||||
import streamDisplay from "./streamContent.vue";
|
import streamDisplay from "./streamContent.vue";
|
||||||
import propertyControl from "../labThingsComponents/propertyControl.vue";
|
|
||||||
import ServerSpecifiedInterface from "../labThingsComponents/serverSpecifiedInterface.vue";
|
|
||||||
import actionLogDisplay from "../labThingsComponents/actionLogDisplay.vue";
|
|
||||||
import actionProgressBar from "../labThingsComponents/actionProgressBar.vue";
|
|
||||||
import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
|
||||||
import ActionButton from "../labThingsComponents/actionButton.vue";
|
import ActionButton from "../labThingsComponents/actionButton.vue";
|
||||||
import { useIntersectionObserver } from "@vueuse/core";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SlideScanContent",
|
name: "SlideScanContent",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
actionTab,
|
||||||
|
slideScanControls,
|
||||||
streamDisplay,
|
streamDisplay,
|
||||||
propertyControl,
|
|
||||||
actionLogDisplay,
|
|
||||||
actionProgressBar,
|
|
||||||
MiniStreamDisplay,
|
|
||||||
ActionButton,
|
ActionButton,
|
||||||
ServerSpecifiedInterface,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lastScanName: null,
|
lastScanName: null,
|
||||||
scanning: false,
|
taskId: null,
|
||||||
taskStatus: "pending",
|
taskUrl: null,
|
||||||
correlateStatus: "",
|
|
||||||
stitchFromStageStatus: "",
|
|
||||||
progress: null,
|
|
||||||
log: [],
|
|
||||||
lastStitchedImage: null,
|
lastStitchedImage: null,
|
||||||
|
scanComplete: false,
|
||||||
scan_name: "",
|
scan_name: "",
|
||||||
workflowName: undefined,
|
|
||||||
workflowSettings: [],
|
|
||||||
workflowOptions: [],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cancellable() {
|
scanning() {
|
||||||
return (this.taskStatus == "running") | (this.taskStatus == "pending");
|
return this.taskId && this.taskUrl;
|
||||||
},
|
},
|
||||||
displayImageOnRight() {
|
displayImageOnRight() {
|
||||||
return this.scanning & (this.lastStitchedImage !== null);
|
return this.scanning && this.lastStitchedImage !== null;
|
||||||
|
},
|
||||||
|
infoPaneTitle() {
|
||||||
|
if (!this.displayImageOnRight) return null;
|
||||||
|
return "Live stitching preview";
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
async created() {
|
|
||||||
this.readSettings();
|
|
||||||
this.workflowOptions = await this.readThingProperty(
|
|
||||||
"smart_scan",
|
|
||||||
"workflow_display_names",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
useIntersectionObserver(
|
|
||||||
this.$refs.slideScanView,
|
|
||||||
([{ isIntersecting }]) => {
|
|
||||||
this.visibilityChanged(isIntersecting);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
visibilityChanged(isVisible) {
|
|
||||||
if (isVisible) {
|
|
||||||
this.readSettings();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async readSettings() {
|
|
||||||
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name");
|
|
||||||
|
|
||||||
if (!this.workflowName) {
|
|
||||||
console.warn("Could not read workflow_name, using default");
|
|
||||||
this.workflowName = "histo_scan_workflow";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.workflowName) {
|
|
||||||
this.ready = await this.readThingProperty(this.workflowName, "ready", true);
|
|
||||||
this.workflowSettings =
|
|
||||||
(await this.getThingEndpoint(this.workflowName, "settings_ui")) || [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onScanError: function (error) {
|
|
||||||
this.scanRunning = false;
|
|
||||||
this.modalError(error);
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Transition the UI into "scanning" mode and begin polling for scan updates.
|
* Transition the UI into "scanning" mode and begin polling for scan updates.
|
||||||
*
|
*
|
||||||
|
|
@ -220,13 +116,25 @@ export default {
|
||||||
* - clears any previous preview image
|
* - clears any previous preview image
|
||||||
* - starts the polling loop that fetches scan progress and preview images
|
* - starts the polling loop that fetches scan progress and preview images
|
||||||
*/
|
*/
|
||||||
startScanning() {
|
startScanning(id, url) {
|
||||||
this.lastStitchedImage = null;
|
this.lastStitchedImage = null;
|
||||||
this.scanning = true;
|
this.lastScanName = null;
|
||||||
|
this.taskId = id;
|
||||||
|
this.taskUrl = url;
|
||||||
|
this.scanComplete = false;
|
||||||
setTimeout(this.pollScan, 1000);
|
setTimeout(this.pollScan, 1000);
|
||||||
},
|
},
|
||||||
|
onScanCompleted() {
|
||||||
|
this.scanComplete = true;
|
||||||
|
},
|
||||||
|
closeTask() {
|
||||||
|
this.taskId = null;
|
||||||
|
this.taskUrl = null;
|
||||||
|
this.lastStitchedImage = null;
|
||||||
|
this.scanComplete = false;
|
||||||
|
},
|
||||||
async pollScan() {
|
async pollScan() {
|
||||||
if (this.cancellable) {
|
if (!this.scanComplete) {
|
||||||
// while the scan is running
|
// while the scan is running
|
||||||
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time", true);
|
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time", true);
|
||||||
if (mtime !== null) {
|
if (mtime !== null) {
|
||||||
|
|
@ -237,21 +145,6 @@ export default {
|
||||||
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
|
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setWorkflow(name) {
|
|
||||||
try {
|
|
||||||
this.workflowName = name;
|
|
||||||
|
|
||||||
await this.writeThingProperty("smart_scan", "workflow_name", name);
|
|
||||||
|
|
||||||
// refresh UI
|
|
||||||
await this.readSettings();
|
|
||||||
} catch (err) {
|
|
||||||
this.modalError(err);
|
|
||||||
|
|
||||||
// revert if server rejected
|
|
||||||
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name", true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async downloadZipFile(response) {
|
async downloadZipFile(response) {
|
||||||
const scan_name = response.input.scan_name;
|
const scan_name = response.input.scan_name;
|
||||||
const filename = `${scan_name}_images.zip`;
|
const filename = `${scan_name}_images.zip`;
|
||||||
|
|
@ -266,11 +159,4 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
#log-display {
|
|
||||||
height: 20em;
|
|
||||||
}
|
|
||||||
.control-component {
|
|
||||||
width: 33%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,20 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Find and ongoing Action and return its task response.
|
||||||
|
*
|
||||||
|
* Returns null if there is no ongoing action
|
||||||
|
*/
|
||||||
|
async getOngingAction(thing, action) {
|
||||||
|
let response = await this.findOngoingActions(thing, action);
|
||||||
|
// Exit if response is null, due to an error.
|
||||||
|
if (response == null) return null;
|
||||||
|
// Check for a task that is ongoing.
|
||||||
|
// We can't handle multiple tasks ongoing, so this picks the first.
|
||||||
|
// ?? Is the "Nullish Coalescing-Operator" to turn undefined into null.
|
||||||
|
return response.data.find((t) => ["pending", "running"].includes(t.status)) ?? 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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue