From 34e46e05eef3330f4e48aff88e8002d12e15ea8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Wed, 11 Oct 2023 13:35:05 +0200 Subject: [PATCH] feat: adding ColouredEnum component --- .../src/components/ColouredEnumComponent.tsx | 75 +++++++++++++++++++ frontend/src/components/GenericComponent.tsx | 17 ++++- src/pydase/components/__init__.py | 2 + src/pydase/components/coloured_enum.py | 61 +++++++++++++++ src/pydase/data_service/data_service.py | 6 +- src/pydase/utils/warnings.py | 5 +- 6 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/ColouredEnumComponent.tsx create mode 100644 src/pydase/components/coloured_enum.py diff --git a/frontend/src/components/ColouredEnumComponent.tsx b/frontend/src/components/ColouredEnumComponent.tsx new file mode 100644 index 0000000..697da4f --- /dev/null +++ b/frontend/src/components/ColouredEnumComponent.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useRef } from 'react'; +import { InputGroup, Form, Row, Col } from 'react-bootstrap'; +import { emit_update } from '../socket'; +import { DocStringComponent } from './DocStringComponent'; + +interface ColouredEnumComponentProps { + name: string; + parentPath: string; + value: string; + docString?: string; + readOnly: boolean; + enumDict: Record; + addNotification: (string) => void; +} + +export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentProps) => { + const { + name, + parentPath: parentPath, + value, + docString, + enumDict, + readOnly, + addNotification + } = props; + const renderCount = useRef(0); + + useEffect(() => { + renderCount.current++; + }); + + useEffect(() => { + addNotification(`${parentPath}.${name} changed to ${value}.`); + }, [props.value]); + + const handleValueChange = (newValue) => { + console.log(newValue); + emit_update(name, parentPath, newValue); + }; + + return ( +
+ {process.env.NODE_ENV === 'development' && ( +

Render count: {renderCount.current}

+ )} + + + + {name} + {readOnly ? ( + // Display the Form.Control when readOnly is true + + ) : ( + // Display the Form.Select when readOnly is false + handleValueChange(event.target.value)}> + {Object.entries(enumDict).map(([key, val]) => ( + + ))} + + )} + + +
+ ); +}); diff --git a/frontend/src/components/GenericComponent.tsx b/frontend/src/components/GenericComponent.tsx index 9bcb462..4958c59 100644 --- a/frontend/src/components/GenericComponent.tsx +++ b/frontend/src/components/GenericComponent.tsx @@ -9,6 +9,7 @@ import { StringComponent } from './StringComponent'; import { ListComponent } from './ListComponent'; import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent'; import { ImageComponent } from './ImageComponent'; +import { ColouredEnumComponent } from './ColouredEnumComponent'; type AttributeType = | 'str' @@ -21,7 +22,8 @@ type AttributeType = | 'DataService' | 'Enum' | 'NumberSlider' - | 'Image'; + | 'Image' + | 'ColouredEnum'; type ValueType = boolean | string | number | object; export interface Attribute { @@ -181,6 +183,19 @@ export const GenericComponent = React.memo( addNotification={addNotification} /> ); + } else if (attribute.type === 'ColouredEnum') { + console.log(attribute); + return ( + + ); } else { return
{name}
; } diff --git a/src/pydase/components/__init__.py b/src/pydase/components/__init__.py index dba223d..da271bf 100644 --- a/src/pydase/components/__init__.py +++ b/src/pydase/components/__init__.py @@ -27,10 +27,12 @@ print(my_service.voltage.value) # Output: 5 ``` """ +from pydase.components.coloured_enum import ColouredEnum from pydase.components.image import Image from pydase.components.number_slider import NumberSlider __all__ = [ "NumberSlider", "Image", + "ColouredEnum", ] diff --git a/src/pydase/components/coloured_enum.py b/src/pydase/components/coloured_enum.py new file mode 100644 index 0000000..e75b9bd --- /dev/null +++ b/src/pydase/components/coloured_enum.py @@ -0,0 +1,61 @@ +from enum import StrEnum + + +class ColouredEnum(StrEnum): + """ + Represents a UI element that can display colour-coded text based on its value. + + This class extends the standard StrEnum but requires its values to be valid CSS + colour codes. Supported colour formats include: + - Hexadecimal colours + - Hexadecimal colours with transparency + - RGB colours + - RGBA colours + - HSL colours + - HSLA colours + - Predefined/Cross-browser colour names + Refer to the this website for more details on colour formats: + (https://www.w3schools.com/cssref/css_colours_legal.php) + + The behavior of this component in the UI depends on how it's defined in the data + service: + - As property with a setter or as attribute: Renders as a dropdown menu, + allowing users to select and change its value from the frontend. + - As property without a setter: Displays as a coloured box with the key of the + `ColouredEnum` as text inside, serving as a visual indicator without user + interaction. + + Example: + -------- + ```python + import pydase.components as pyc + import pydase + + class MyStatus(pyc.ColouredEnum): + PENDING = "#FFA500" # Orange + RUNNING = "#0000FF80" # Transparent Blue + PAUSED = "rgb(169, 169, 169)" # Dark Gray + RETRYING = "rgba(255, 255, 0, 0.3)" # Transparent Yellow + COMPLETED = "hsl(120, 100%, 50%)" # Green + FAILED = "hsla(0, 100%, 50%, 0.7)" # Transparent Red + CANCELLED = "SlateGray" # Slate Gray + + class StatusExample(pydase.DataService): + _status = MyStatus.RUNNING + + @property + def status(self) -> MyStatus: + return self._status + + @status.setter + def status(self, value: MyStatus) -> None: + # Custom logic here... + self._status = value + + # Example usage: + my_service = StatusExample() + my_service.status = MyStatus.FAILED + ``` + """ + + pass diff --git a/src/pydase/data_service/data_service.py b/src/pydase/data_service/data_service.py index 06dd197..8dace1e 100644 --- a/src/pydase/data_service/data_service.py +++ b/src/pydase/data_service/data_service.py @@ -311,8 +311,12 @@ class DataService(rpyc.Service, AbstractDataService): "value": running_task_info, } elif isinstance(value, Enum): + if type(value).__base__.__name__ == "ColouredEnum": + val_type = "ColouredEnum" + else: + val_type = "Enum" result[key] = { - "type": "Enum", + "type": val_type, "value": value.name, "enum": { name: member.value diff --git a/src/pydase/utils/warnings.py b/src/pydase/utils/warnings.py index c7e587c..dd1a033 100644 --- a/src/pydase/utils/warnings.py +++ b/src/pydase/utils/warnings.py @@ -1,5 +1,7 @@ from loguru import logger +from pydase.utils.helpers import get_component_class_names + def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) -> None: base_class_name = __value.__class__.__base__.__name__ @@ -13,7 +15,8 @@ def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) -> "asyncio.unix_events", "_abc", ] - and base_class_name not in ["DataService", "list", "Enum"] + and base_class_name + not in ["DataService", "list", "Enum"] + get_component_class_names() and type(__value).__name__ not in ["CallbackManager", "TaskManager", "Quantity"] ): logger.warning(