diff --git a/README.md b/README.md index d272df4..a59c915 100644 --- a/README.md +++ b/README.md @@ -231,57 +231,105 @@ The custom components in `pydase` have two main parts: Below are the components available in the `pydase.components` module, accompanied by their Python usage: -- `Image`: This component allows users to display and update images within the application. +#### `Image` - ```python - import matplotlib.pyplot as plt - import numpy as np +This component provides a versatile interface for displaying images within the application. Users can update and manage images from various sources, including local paths, URLs, and even matplotlib figures. - import pydase - from pydase.components.image import Image +The component offers methods to load images seamlessly, ensuring that visual content is easily integrated and displayed within the data service. + +```python +import matplotlib.pyplot as plt +import numpy as np + +import pydase +from pydase.components.image import Image - class MyDataService(pydase.DataService): - my_image = Image() +class MyDataService(pydase.DataService): + my_image = Image() - if __name__ == "__main__": - service = MyDataService() - # loading from local path - service.my_image.load_from_path("/your/image/path/") +if __name__ == "__main__": + service = MyDataService() + # loading from local path + service.my_image.load_from_path("/your/image/path/") - # loading from a URL - service.my_image.load_from_url("https://cataas.com/cat") + # loading from a URL + service.my_image.load_from_url("https://cataas.com/cat") - # loading a matplotlib figure - fig = plt.figure() - x = np.linspace(0, 2 * np.pi) - plt.plot(x, np.sin(x)) - plt.grid() - service.my_image.load_from_matplotlib_figure(fig) + # loading a matplotlib figure + fig = plt.figure() + x = np.linspace(0, 2 * np.pi) + plt.plot(x, np.sin(x)) + plt.grid() + service.my_image.load_from_matplotlib_figure(fig) - pydase.Server(service).run() - ``` + pydase.Server(service).run() +``` - ![Image Component](docs/images/Image_component.png) +![Image Component](docs/images/Image_component.png) -- `NumberSlider`: An interactive slider component to adjust numerical values, including floats and integers, on the frontend while synchronizing the data with the backend in real-time. +#### `NumberSlider` - ```python - import pydase - from pydase.components import NumberSlider + This component provides an interactive slider interface for adjusting numerical values on the frontend. It supports both floats and integers. The values adjusted on the frontend are synchronized with the backend in real-time, ensuring consistent data representation. + +The slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases. + +```python +import pydase +from pydase.components import NumberSlider - class MyService(pydase.DataService): - slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1) +class MyService(pydase.DataService): + slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1, type="float") - if __name__ == "__main__": - service = MyService() - pydase.Server(service).run() - ``` +if __name__ == "__main__": + service = MyService() + pydase.Server(service).run() +``` - ![Slider Component](docs/images/Slider_component.png) +![Slider Component](docs/images/Slider_component.png) + +#### `ColouredEnum` + +This component provides a way to visually represent different states or categories in a data service using colour-coded options. It behaves similarly to a standard `Enum`, but the values encode colours in a format understood by CSS. The colours can be defined using various methods like Hexadecimal, RGB, HSL, and more. + +If the property associated with the `ColouredEnum` has a setter function, the keys of the enum will be rendered as a dropdown menu, allowing users to interact and select different options. Without a setter function, the selected key will simply be displayed as a coloured box with text inside, serving as a visual indicator. + +```python +import pydase +import pydase.components as pyc + + +class MyStatus(pyc.ColouredEnum): + PENDING = "#FFA500" # Hexadecimal colour (Orange) + RUNNING = "#0000FF80" # Hexadecimal colour with transparency (Blue) + PAUSED = "rgb(169, 169, 169)" # RGB colour (Dark Gray) + RETRYING = "rgba(255, 255, 0, 0.3)" # RGB colour with transparency (Yellow) + COMPLETED = "hsl(120, 100%, 50%)" # HSL colour (Green) + FAILED = "hsla(0, 100%, 50%, 0.7)" # HSL colour with transparency (Red) + CANCELLED = "SlateGray" # Cross-browser colour name (Slate Gray) + + +class StatusTest(pydase.DataService): + _status = MyStatus.RUNNING + + @property + def status(self) -> MyStatus: + return self._status + + @status.setter + def status(self, value: MyStatus) -> None: + # do something ... + self._status = value + +# Modifying or accessing the status value: +my_service = StatusExample() +my_service.status = MyStatus.FAILED +``` + +![ColouredEnum Component](docs/images/ColouredEnum_component.png) #### Extending with New Components diff --git a/docs/images/ColouredEnum_component.png b/docs/images/ColouredEnum_component.png new file mode 100644 index 0000000..e63c0f1 Binary files /dev/null and b/docs/images/ColouredEnum_component.png differ 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..40648c4 --- /dev/null +++ b/src/pydase/components/coloured_enum.py @@ -0,0 +1,61 @@ +from enum import Enum + + +class ColouredEnum(Enum): + """ + Represents a UI element that can display colour-coded text based on its value. + + This class extends the standard Enum 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/frontend/asset-manifest.json b/src/pydase/frontend/asset-manifest.json index c471e70..4505ece 100644 --- a/src/pydase/frontend/asset-manifest.json +++ b/src/pydase/frontend/asset-manifest.json @@ -1,13 +1,13 @@ { "files": { "main.css": "/static/css/main.398bc7f8.css", - "main.js": "/static/js/main.c348625e.js", + "main.js": "/static/js/main.553ca0c9.js", "index.html": "/index.html", "main.398bc7f8.css.map": "/static/css/main.398bc7f8.css.map", - "main.c348625e.js.map": "/static/js/main.c348625e.js.map" + "main.553ca0c9.js.map": "/static/js/main.553ca0c9.js.map" }, "entrypoints": [ "static/css/main.398bc7f8.css", - "static/js/main.c348625e.js" + "static/js/main.553ca0c9.js" ] } \ No newline at end of file diff --git a/src/pydase/frontend/index.html b/src/pydase/frontend/index.html index a4becaf..84b400a 100644 --- a/src/pydase/frontend/index.html +++ b/src/pydase/frontend/index.html @@ -1 +1 @@ -pydase App
\ No newline at end of file +pydase App
\ No newline at end of file diff --git a/src/pydase/frontend/static/js/main.c348625e.js b/src/pydase/frontend/static/js/main.553ca0c9.js similarity index 92% rename from src/pydase/frontend/static/js/main.c348625e.js rename to src/pydase/frontend/static/js/main.553ca0c9.js index 4a90552..547eb75 100644 --- a/src/pydase/frontend/static/js/main.c348625e.js +++ b/src/pydase/frontend/static/js/main.553ca0c9.js @@ -1,3 +1,3 @@ -/*! For license information please see main.c348625e.js.LICENSE.txt */ -!function(){var e={694:function(e,t){var n;!function(){"use strict";var r={}.hasOwnProperty;function a(){for(var e=[],t=0;t