Implement recursive Interface in vue

This commit is contained in:
Julian Stirling 2026-03-08 11:06:04 +00:00
parent a11f9b7f62
commit 9f2cde2275
6 changed files with 167 additions and 57 deletions

View file

@ -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):

View file

@ -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,
}
}

View 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>

View file

@ -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>

View file

@ -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",

View file

@ -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;
}
},
}, },
}; };