Tidy up server specified interface

This commit is contained in:
Julian Stirling 2026-03-08 11:39:50 +00:00
parent 9f2cde2275
commit ca21ab03c6
5 changed files with 49 additions and 36 deletions

View file

@ -13,7 +13,11 @@ from pydantic import BaseModel
import labthings_fastapi as lt import labthings_fastapi as lt
from openflexure_microscope_server.ui import PropertyControl, property_control_for from openflexure_microscope_server.ui import (
UI_ELEMENT_RESPONSE,
UIElementList,
property_control_for,
)
SettingsType = TypeVar("SettingsType", bound=BaseModel) SettingsType = TypeVar("SettingsType", bound=BaseModel)
BackgroundType = TypeVar("BackgroundType", bound=BaseModel) BackgroundType = TypeVar("BackgroundType", bound=BaseModel)
@ -72,9 +76,9 @@ class BackgroundDetectAlgorithm(lt.Thing):
"Each background detect algorithm must implement an set_background method." "Each background detect algorithm must implement an set_background method."
) )
@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 UI.""" """Return the UI for the background detect settings."""
raise NotImplementedError( raise NotImplementedError(
"Each background detect algorithm must implement an settings_ui method." "Each background detect algorithm must implement an settings_ui method."
) )
@ -113,15 +117,19 @@ class ColourChannelDetectLUV(BackgroundDetectAlgorithm):
image to be labeled as containing sample. image to be labeled as containing sample.
""" """
@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 UI.""" """Return the UI for the background detect settings."""
return [ return UIElementList(
property_control_for(self, "channel_tolerance", label="Channel Tolerance"), [
property_control_for( property_control_for(
self, "min_sample_coverage", label="Sample Coverage Required (%)" self, "channel_tolerance", label="Channel Tolerance"
), ),
] property_control_for(
self, "min_sample_coverage", label="Sample Coverage Required (%)"
),
]
)
@lt.property @lt.property
def ready(self) -> bool: def ready(self) -> bool:
@ -237,15 +245,19 @@ class ChannelDeviationLUV(BackgroundDetectAlgorithm):
image to be labeled as containing sample. image to be labeled as containing sample.
""" """
@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 UI.""" """Return the UI for the background detect settings."""
return [ return UIElementList(
property_control_for(self, "channel_tolerance", label="Channel Tolerance"), [
property_control_for( property_control_for(
self, "min_sample_coverage", label="Sample Coverage Required (%)" self, "channel_tolerance", label="Channel Tolerance"
), ),
] property_control_for(
self, "min_sample_coverage", label="Sample Coverage Required (%)"
),
]
)
@lt.property @lt.property
def ready(self) -> bool: def ready(self) -> bool:

View file

@ -566,7 +566,7 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
[ [
Accordion( Accordion(
title="Background Detect", title="Background Detect",
children=self._background_detector.settings_ui, children=self._background_detector.settings_ui(),
), ),
property_control_for( property_control_for(
self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05 self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05
@ -581,7 +581,7 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
), ),
property_control_for( property_control_for(
self, "stack_dz", label="Stack dz (steps)", step=5 self, "stack_dz", label="Stack dz (steps)", step=5
).model_dump(), ),
property_control_for( property_control_for(
self, "autofocus_dz", label="Autofocus Range (steps)", step=200 self, "autofocus_dz", label="Autofocus Range (steps)", step=200
), ),

View file

@ -32,7 +32,7 @@ class SafeHTMLParser(HTMLParser):
super().__init__() super().__init__()
self.output = "" self.output = ""
def handle_starttag(self, tag: str, attrs: dict[str, str]) -> None: def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
"""Append a start tag found in the HTML if it is in the allowed list. """Append a start tag found in the HTML if it is in the allowed list.
This is called by the base class when a start tag is found. We only append This is called by the base class when a start tag is found. We only append
@ -52,15 +52,16 @@ class SafeHTMLParser(HTMLParser):
if tag == "a": if tag == "a":
href = None href = None
for attr, value in attrs: for attr, value in attrs:
if attr == "href": if attr == "href" and value is not None:
href = value.strip() href = value.strip()
if href: if href:
safe_href = escape(href, quote=True) safe_href = escape(href, quote=True)
# Always in a new tab # Always in a new tab
self.output.append( self.output += (
f'<a href="{safe_href}" target="_blank" rel="noopener noreferrer">' f'<a href="{safe_href}" target="_blank" rel="noopener noreferrer">'
) )
else: else:
self.output += "<a>" self.output += "<a>"
else: else:
@ -71,7 +72,9 @@ class SafeHTMLParser(HTMLParser):
if tag in ALLOWED_TAGS: if tag in ALLOWED_TAGS:
self.output += f"</{tag}>" self.output += f"</{tag}>"
def handle_startendtag(self, tag: str, _attrs: dict[str, str]) -> None: def handle_startendtag(
self, tag: str, _attrs: list[tuple[str, Optional[str]]]
) -> None:
"""Append a self-closing tag if it is a new line.""" """Append a self-closing tag if it is a new line."""
if tag == "br": if tag == "br":
self.output += "<br/>" self.output += "<br/>"
@ -251,7 +254,7 @@ class Accordion(BaseModel):
element_type: Literal["accordion"] = "accordion" element_type: Literal["accordion"] = "accordion"
title: str title: str
children: list["UIElementModels"] children: "UIElementList"
class Container(BaseModel): class Container(BaseModel):
@ -262,7 +265,7 @@ class Container(BaseModel):
element_type: Literal["container"] = "container" element_type: Literal["container"] = "container"
css_class: str css_class: str
children: list["UIElementModels"] children: "UIElementList"
UIElementModels = HTMLBlock | PropertyControl | ActionButton | Accordion | Container UIElementModels = HTMLBlock | PropertyControl | ActionButton | Accordion | Container

View file

@ -1,6 +1,8 @@
<template> <template>
<div v-for="(element, index) in elements" :key="index" class="uk-margin"> <div v-for="(element, index) in elements" :key="index" class="uk-margin">
<!-- eslint-disable vue/no-v-html -->
<div v-if="element.element_type === 'html_block'" v-html="element.html"></div> <div v-if="element.element_type === 'html_block'" v-html="element.html"></div>
<!-- eslint-enable -->
<server-specified-property-control <server-specified-property-control
v-else-if="element.element_type === 'property_control'" v-else-if="element.element_type === 'property_control'"
:property-data="element" :property-data="element"
@ -12,11 +14,9 @@
<simple-accordion v-else-if="element.element_type === 'accordion'" :title="element.title"> <simple-accordion v-else-if="element.element_type === 'accordion'" :title="element.title">
<server-specified-interface :elements="element.children" /> <server-specified-interface :elements="element.children" />
</simple-accordion> </simple-accordion>
<!-- eslint-disable vue/no-v-html -->
<div v-else-if="element.element_type === 'container'" :class="element.css_class"> <div v-else-if="element.element_type === 'container'" :class="element.css_class">
<server-specified-interface :elements="element.children" /> <server-specified-interface :elements="element.children" />
</div> </div>
<!-- eslint-enable -->
</div> </div>
</template> </template>

View file

@ -111,10 +111,8 @@ export default {
); );
if (this.backgroundDetectorName) { if (this.backgroundDetectorName) {
this.ready = await this.readThingProperty(this.backgroundDetectorName, "ready"); this.ready = await this.readThingProperty(this.backgroundDetectorName, "ready");
this.backgroundDetectorSettings = await this.readThingProperty( this.backgroundDetectorSettings =
this.backgroundDetectorName, (await this.getThingEndpoint(this.backgroundDetectorName, "settings_ui")) || [];
"settings_ui",
);
this.backgroundDetectorDisplayName = await this.readThingProperty( this.backgroundDetectorDisplayName = await this.readThingProperty(
this.backgroundDetectorName, this.backgroundDetectorName,
"display_name", "display_name",