`` is allowed with ``href`` only. This automatically
appends ``target="_blank" rel="noopener noreferrer"`` so the link opens externally.
Self closing tags allowed ``
``.
:param html: The input HTML.
:return: The sanitised HTML.
"""
parser = SafeHTMLParser()
parser.feed(html)
parser.close()
return parser.output
HtmlFragment = Annotated[str, BeforeValidator(sanitise_html)]
class HeaderBlock(BaseModel):
"""The data required for header."""
element_type: Literal["header_block"] = "header_block"
text: HtmlFragment
"""The header text (can include basic HTML tags)."""
level: int = Field(default=2, ge=1, le=6)
"""The header level. A number from 1-6."""
class TextBlock(BaseModel):
"""The data required for creating a block text."""
element_type: Literal["text_block"] = "text_block"
text: HtmlFragment
"""The text (can include basic HTML tags)."""
class BulletBlock(BaseModel):
"""The data required for creating a block text."""
element_type: Literal["bullet_block"] = "bullet_block"
bullets: list[HtmlFragment]
"""A list of text elements (can include basic HTML tags)."""
class ActionButton(BaseModel):
"""The data required for creating an actionButton in Vue.
Currently this cannot be used to submitData, or to submitOnEvent.
"""
element_type: Literal["action_button"] = "action_button"
thing: str
"""The Thing "path" for the Thing instance."""
action: str
"""The name of the action to be triggered."""
poll_interval: float = 1
"""The interval for polling in seconds."""
submit_label: str = "Submit"
"""The label for the action button."""
can_terminate: bool = True
"""Specify whether the action can be terminated."""
requires_confirmation: bool = False
"""Specify whether a confirmation modal is needed."""
confirmation_message: str = "Start task?"
"""The message for the confirmation modal."""
button_primary: bool = True
"""Specify whether the button is styled as a primary button."""
disabled: bool = False
"""Specify whether the button is disabled."""
modal_progress: bool = False
"""Specify whether to show a progress modal."""
stream_with_modal: bool = False
"""Specify whether to show a mini stream preview in the progress modal.
This option only has an effect if `modal_progress` is True.
If `modal_progress` is False, no modal (and therefore no stream preview) is shown.
"""
notify_on_success: bool = False
"""Specify whether to a notification on successful completion."""
response_is_success_message: bool = False
"""Set True to use the action response as the success message.
If ``success_message`` is set, this is never used.
"""
success_message: str = "Success!"
"""The message to show on successful completion."""
update_interface_on_response: bool = False
"""If True the action button emits a requestUpdate signal when completed.
Downstream components can subscribe to this and request an update.
"""
broken: bool = False
"""If True the action button is broken. There is no corresponding action."""
def action_button_for(thing: lt.Thing, action_name: str, **kwargs: Any) -> ActionButton:
"""Create a ActionButton data for the specified Thing Action.
:param thing: The instance of the thing that has the action.
:param action_name: The name of the action to create a button for.
:param kwargs: Any attribute of `ActionButton` except for ``thing`` or ``action``.
:return: An ActionButton (Pydantic Model) object with all the information the
webapp needs to create the action button.
"""
if action_name not in thing.actions:
thing.logger.error(f"{thing.name} has no action {action_name}")
kwargs["broken"] = True
return ActionButton(thing=thing.name, action=action_name, **kwargs)
class PropertyControl(BaseModel):
"""The data required for creating an actionButton in Vue.
Currently this cannot be used to submitData, or to submitOnEvent.
"""
element_type: Literal["property_control"] = "property_control"
thing: str
"""The Thing "path" for the Thing instance."""
property_name: str
"""The name of the property (or setting)."""
label: str
"""The label to show in the UI"""
read_back: bool = False
"""Whether or not to read back the property after setting.
This is useful for hardware settings that may be coerced to the closest value.
"""
read_back_delay: int = 1000
"""The delay in ms before reading back the property."""
options: Optional[dict[str, str | int | float | bool]] = None
"""A mapping of UI display name to value, used for creating a dropdown.
These options aren't validated here in any way. Any invalid values will be rejected
when selected.
"""
step: Optional[int | float] = None
"""The step size for a numerical input.
If the property is not numeric this will be ignored.
If this is left as None, then the UI will try to use the ``multipleOf`` field in
the property dataSchema. If ``multipleOf`` is not set then the browser default is
used.
"""
broken: bool = False
"""If True the property control is broken. There is no corresponding property."""
def property_control_for(
thing: lt.Thing, property_name: str, **kwargs: Any
) -> PropertyControl:
"""Create an PropertyControl data for the specified Thing Property.
:param thing: The instance of the thing that has the property to be controlled.
:param property_name: The name of the property to create a control for.
:param kwargs: Any attribute of `PropertyControl` except for ``thing`` or
``property_name``. If label is not set here it will be the property name.
"""
if "label" not in kwargs:
kwargs["label"] = property_name
if property_name not in thing.properties:
thing.logger.error(f"{thing.name} has no property {property_name}")
kwargs["broken"] = True
return PropertyControl(thing=thing.name, property_name=property_name, **kwargs)
class Accordion(BaseModel):
"""The data required for creating an accordion in the UI.
The HTML is limited to a small number of tags.
"""
element_type: Literal["accordion"] = "accordion"
title: str
children: "UIElementList"
class Container(BaseModel):
"""The data required for creating ```` in the UI.
The HTML is limited to a small number of tags.
"""
element_type: Literal["container"] = "container"
css_class: str
children: "UIElementList"
UIElementModels = (
HeaderBlock
| TextBlock
| BulletBlock
| 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 recursion. Use an
lt.endpoint instead
* If the module using this class imports future annotations then this will cause
havoc with pydantics forward references.
"""
# Resolve forward references so Pydantic can build a sensible recursive schema
Accordion.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,
}
}