Implement recursive Interface in vue
This commit is contained in:
parent
a11f9b7f62
commit
9f2cde2275
6 changed files with 167 additions and 57 deletions
|
|
@ -4,8 +4,6 @@ This module contains the base ``ScanWorkflow`` class that all workflows should s
|
||||||
as well as specific workflows.
|
as well as specific workflows.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import (
|
from typing import (
|
||||||
Generic,
|
Generic,
|
||||||
|
|
@ -41,7 +39,12 @@ from openflexure_microscope_server.things.background_detect import (
|
||||||
from openflexure_microscope_server.things.camera import BaseCamera, CaptureParams
|
from openflexure_microscope_server.things.camera import BaseCamera, CaptureParams
|
||||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||||
from openflexure_microscope_server.things.stage import BaseStage
|
from openflexure_microscope_server.things.stage import BaseStage
|
||||||
from openflexure_microscope_server.ui import PropertyControl, property_control_for
|
from openflexure_microscope_server.ui import (
|
||||||
|
UI_ELEMENT_RESPONSE,
|
||||||
|
Accordion,
|
||||||
|
UIElementList,
|
||||||
|
property_control_for,
|
||||||
|
)
|
||||||
|
|
||||||
SettingModelType = TypeVar("SettingModelType", bound=BaseModel)
|
SettingModelType = TypeVar("SettingModelType", bound=BaseModel)
|
||||||
|
|
||||||
|
|
@ -160,9 +163,9 @@ class ScanWorkflow(Generic[SettingModelType], lt.Thing):
|
||||||
|
|
||||||
return True, focus_height
|
return True, focus_height
|
||||||
|
|
||||||
@lt.property
|
@lt.endpoint("get", "settings_ui", responses=UI_ELEMENT_RESPONSE)
|
||||||
def settings_ui(self) -> list[PropertyControl]:
|
def settings_ui(self) -> UIElementList:
|
||||||
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
"""Return the UI for the workflow's settings in the scan tab."""
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"Each scan workflow must implement a settings_ui method."
|
"Each scan workflow must implement a settings_ui method."
|
||||||
)
|
)
|
||||||
|
|
@ -556,35 +559,43 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
|
||||||
|
|
||||||
return imaged, focus_height
|
return imaged, focus_height
|
||||||
|
|
||||||
@lt.property
|
@lt.endpoint("get", "settings_ui", responses=UI_ELEMENT_RESPONSE)
|
||||||
def settings_ui(self) -> list[PropertyControl]:
|
def settings_ui(self) -> UIElementList:
|
||||||
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
"""Return the UI for the workflow's settings in the scan tab."""
|
||||||
return [
|
return UIElementList(
|
||||||
property_control_for(
|
[
|
||||||
self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05
|
Accordion(
|
||||||
),
|
title="Background Detect",
|
||||||
property_control_for(
|
children=self._background_detector.settings_ui,
|
||||||
self, "stack_images_to_save", label="Images in Stack to Save"
|
),
|
||||||
),
|
property_control_for(
|
||||||
property_control_for(
|
self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05
|
||||||
self,
|
),
|
||||||
"stack_min_images_to_test",
|
property_control_for(
|
||||||
label="Minimum number of images to test for focus",
|
self, "stack_images_to_save", label="Images in Stack to Save"
|
||||||
),
|
),
|
||||||
property_control_for(self, "stack_dz", label="Stack dz (steps)", step=5),
|
property_control_for(
|
||||||
property_control_for(
|
self,
|
||||||
self, "autofocus_dz", label="Autofocus Range (steps)", step=200
|
"stack_min_images_to_test",
|
||||||
),
|
label="Minimum number of images to test for focus",
|
||||||
property_control_for(
|
),
|
||||||
self, "max_range", label="Maximum Distance (steps)", step=1000
|
property_control_for(
|
||||||
),
|
self, "stack_dz", label="Stack dz (steps)", step=5
|
||||||
property_control_for(
|
).model_dump(),
|
||||||
self, "skip_background", label="Detect and Skip Empty Fields"
|
property_control_for(
|
||||||
),
|
self, "autofocus_dz", label="Autofocus Range (steps)", step=200
|
||||||
property_control_for(
|
),
|
||||||
self, "equal_distances", label="Set Equal x and y Distances"
|
property_control_for(
|
||||||
),
|
self, "max_range", label="Maximum Distance (steps)", step=1000
|
||||||
]
|
),
|
||||||
|
property_control_for(
|
||||||
|
self, "skip_background", label="Detect and Skip Empty Fields"
|
||||||
|
),
|
||||||
|
property_control_for(
|
||||||
|
self, "equal_distances", label="Set Equal x and y Distances"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegularGridSettingsModel(RectGridSettingsModel):
|
class RegularGridSettingsModel(RectGridSettingsModel):
|
||||||
|
|
@ -668,17 +679,21 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]):
|
||||||
save_resolution=settings.capture_params.save_resolution,
|
save_resolution=settings.capture_params.save_resolution,
|
||||||
)
|
)
|
||||||
|
|
||||||
@lt.property
|
@lt.endpoint("get", "settings_ui", responses=UI_ELEMENT_RESPONSE)
|
||||||
def settings_ui(self) -> list[PropertyControl]:
|
def settings_ui(self) -> UIElementList:
|
||||||
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
"""Return the UI for the workflow's settings in the scan tab."""
|
||||||
return [
|
return UIElementList(
|
||||||
property_control_for(
|
[
|
||||||
self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05
|
property_control_for(
|
||||||
),
|
self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05
|
||||||
property_control_for(self, "x_count", label="Number of columns"),
|
),
|
||||||
property_control_for(self, "y_count", label="Number of rows"),
|
property_control_for(self, "x_count", label="Number of columns"),
|
||||||
property_control_for(self, "autofocus_dz", label="Autofocus Range (steps)"),
|
property_control_for(self, "y_count", label="Number of rows"),
|
||||||
]
|
property_control_for(
|
||||||
|
self, "autofocus_dz", label="Autofocus Range (steps)"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SnakeWorkflow(RegularGridWorkflow):
|
class SnakeWorkflow(RegularGridWorkflow):
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from html import escape
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from typing import Annotated, Any, Literal, Optional
|
from typing import Annotated, Any, Literal, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, BeforeValidator
|
from pydantic import BaseModel, BeforeValidator, RootModel
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
|
|
||||||
|
|
@ -268,6 +268,28 @@ class Container(BaseModel):
|
||||||
UIElementModels = HTMLBlock | PropertyControl | ActionButton | Accordion | Container
|
UIElementModels = HTMLBlock | PropertyControl | ActionButton | Accordion | Container
|
||||||
|
|
||||||
|
|
||||||
|
class UIElementList(RootModel[list[UIElementModels]]):
|
||||||
|
"""A list of user interface elements.
|
||||||
|
|
||||||
|
Note! Two important things to consider:
|
||||||
|
|
||||||
|
* This cannot be the return of a lt.property. To fully describe the UI recursion
|
||||||
|
is used. Web of Things DataSchema doesn't support rectursion. Use a
|
||||||
|
lt.endpoint instead
|
||||||
|
* If the module using this class imoports future annotations then this will cause
|
||||||
|
havoc with pydantics forward references.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Resolve forward references so Pydantic can build a sensible recursive schema
|
# Resolve forward references so Pydantic can build a sensible recursive schema
|
||||||
Accordion.model_rebuild()
|
Accordion.model_rebuild()
|
||||||
Container.model_rebuild()
|
Container.model_rebuild()
|
||||||
|
UIElementList.model_rebuild()
|
||||||
|
|
||||||
|
# This constant can be used as the value for the `responses` argument of lt.endpoint.
|
||||||
|
UI_ELEMENT_RESPONSE = {
|
||||||
|
200: {
|
||||||
|
"description": "A list of UI element specifications.",
|
||||||
|
"model": UIElementList,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
23
webapp/src/components/genericComponents/simpleAccordion.vue
Normal file
23
webapp/src/components/genericComponents/simpleAccordion.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<ul uk-accordion="multiple: true">
|
||||||
|
<li>
|
||||||
|
<a class="uk-accordion-title" href="#">{{ title }}</a>
|
||||||
|
<div class="uk-accordion-content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "SimpleAccordion",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<div v-for="(element, index) in elements" :key="index" class="uk-margin">
|
||||||
|
<div v-if="element.element_type === 'html_block'" v-html="element.html"></div>
|
||||||
|
<server-specified-property-control
|
||||||
|
v-else-if="element.element_type === 'property_control'"
|
||||||
|
:property-data="element"
|
||||||
|
/>
|
||||||
|
<server-specified-action-button
|
||||||
|
v-else-if="element.element_type === 'action_button'"
|
||||||
|
:property-data="element"
|
||||||
|
/>
|
||||||
|
<simple-accordion v-else-if="element.element_type === 'accordion'" :title="element.title">
|
||||||
|
<server-specified-interface :elements="element.children" />
|
||||||
|
</simple-accordion>
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<div v-else-if="element.element_type === 'container'" :class="element.css_class">
|
||||||
|
<server-specified-interface :elements="element.children" />
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SimpleAccordion from "../genericComponents/simpleAccordion.vue";
|
||||||
|
import ServerSpecifiedPropertyControl from "./serverSpecifiedPropertyControl.vue";
|
||||||
|
import ServerSpecifiedActionButton from "./serverSpecifiedActionButton.vue";
|
||||||
|
|
||||||
|
// Export main app
|
||||||
|
export default {
|
||||||
|
name: "ServerSpecifiedInterface",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
SimpleAccordion,
|
||||||
|
ServerSpecifiedPropertyControl,
|
||||||
|
ServerSpecifiedActionButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
elements: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less"></style>
|
||||||
|
|
@ -24,13 +24,7 @@
|
||||||
<li>
|
<li>
|
||||||
<a class="uk-accordion-title" href="#">Scan Settings</a>
|
<a class="uk-accordion-title" href="#">Scan Settings</a>
|
||||||
<div class="uk-accordion-content">
|
<div class="uk-accordion-content">
|
||||||
<div
|
<server-specified-interface :elements="workflowSettings" />
|
||||||
v-for="(setting, index) in workflowSettings"
|
|
||||||
:key="'detector_setting' + index"
|
|
||||||
class="uk-margin"
|
|
||||||
>
|
|
||||||
<server-specified-property-control :property-data="setting" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="uk-open">
|
<li class="uk-open">
|
||||||
|
|
@ -125,7 +119,7 @@
|
||||||
<script>
|
<script>
|
||||||
import streamDisplay from "./streamContent.vue";
|
import streamDisplay from "./streamContent.vue";
|
||||||
import propertyControl from "../labThingsComponents/propertyControl.vue";
|
import propertyControl from "../labThingsComponents/propertyControl.vue";
|
||||||
import ServerSpecifiedPropertyControl from "../labThingsComponents/serverSpecifiedPropertyControl.vue";
|
import ServerSpecifiedInterface from "../labThingsComponents/serverSpecifiedInterface.vue";
|
||||||
import actionLogDisplay from "../labThingsComponents/actionLogDisplay.vue";
|
import actionLogDisplay from "../labThingsComponents/actionLogDisplay.vue";
|
||||||
import actionProgressBar from "../labThingsComponents/actionProgressBar.vue";
|
import actionProgressBar from "../labThingsComponents/actionProgressBar.vue";
|
||||||
import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
||||||
|
|
@ -142,7 +136,7 @@ export default {
|
||||||
actionProgressBar,
|
actionProgressBar,
|
||||||
MiniStreamDisplay,
|
MiniStreamDisplay,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
ServerSpecifiedPropertyControl,
|
ServerSpecifiedInterface,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -211,7 +205,7 @@ export default {
|
||||||
if (this.workflowName) {
|
if (this.workflowName) {
|
||||||
this.ready = await this.readThingProperty(this.workflowName, "ready", true);
|
this.ready = await this.readThingProperty(this.workflowName, "ready", true);
|
||||||
this.workflowSettings =
|
this.workflowSettings =
|
||||||
(await this.readThingProperty(this.workflowName, "settings_ui", true)) || [];
|
(await this.getThingEndpoint(this.workflowName, "settings_ui")) || [];
|
||||||
this.workflowDisplayName = await this.readThingProperty(
|
this.workflowDisplayName = await this.readThingProperty(
|
||||||
this.workflowName,
|
this.workflowName,
|
||||||
"display_name",
|
"display_name",
|
||||||
|
|
|
||||||
|
|
@ -135,5 +135,14 @@ export default {
|
||||||
);
|
);
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
async getThingEndpoint(thing, endpoint) {
|
||||||
|
let url = `${this.$store.getters.baseUri}/${thing}/${endpoint}`;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue