mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-12 15:57:12 +02:00
feat: adding ColouredEnum component
This commit is contained in:
75
frontend/src/components/ColouredEnumComponent.tsx
Normal file
75
frontend/src/components/ColouredEnumComponent.tsx
Normal file
@ -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<string, string>;
|
||||||
|
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 (
|
||||||
|
<div className={'enumComponent'} id={parentPath.concat('.' + name)}>
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<p>Render count: {renderCount.current}</p>
|
||||||
|
)}
|
||||||
|
<DocStringComponent docString={docString} />
|
||||||
|
<Row>
|
||||||
|
<Col className="d-flex align-items-center">
|
||||||
|
<InputGroup.Text>{name}</InputGroup.Text>
|
||||||
|
{readOnly ? (
|
||||||
|
// Display the Form.Control when readOnly is true
|
||||||
|
<Form.Control
|
||||||
|
value={value}
|
||||||
|
disabled={true}
|
||||||
|
style={{ backgroundColor: enumDict[value] }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// Display the Form.Select when readOnly is false
|
||||||
|
<Form.Select
|
||||||
|
aria-label="coloured-enum-select"
|
||||||
|
value={value}
|
||||||
|
style={{ backgroundColor: enumDict[value] }}
|
||||||
|
onChange={(event) => handleValueChange(event.target.value)}>
|
||||||
|
{Object.entries(enumDict).map(([key, val]) => (
|
||||||
|
<option key={key} value={key}>
|
||||||
|
{key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Select>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -9,6 +9,7 @@ import { StringComponent } from './StringComponent';
|
|||||||
import { ListComponent } from './ListComponent';
|
import { ListComponent } from './ListComponent';
|
||||||
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
|
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
|
||||||
import { ImageComponent } from './ImageComponent';
|
import { ImageComponent } from './ImageComponent';
|
||||||
|
import { ColouredEnumComponent } from './ColouredEnumComponent';
|
||||||
|
|
||||||
type AttributeType =
|
type AttributeType =
|
||||||
| 'str'
|
| 'str'
|
||||||
@ -21,7 +22,8 @@ type AttributeType =
|
|||||||
| 'DataService'
|
| 'DataService'
|
||||||
| 'Enum'
|
| 'Enum'
|
||||||
| 'NumberSlider'
|
| 'NumberSlider'
|
||||||
| 'Image';
|
| 'Image'
|
||||||
|
| 'ColouredEnum';
|
||||||
|
|
||||||
type ValueType = boolean | string | number | object;
|
type ValueType = boolean | string | number | object;
|
||||||
export interface Attribute {
|
export interface Attribute {
|
||||||
@ -181,6 +183,19 @@ export const GenericComponent = React.memo(
|
|||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (attribute.type === 'ColouredEnum') {
|
||||||
|
console.log(attribute);
|
||||||
|
return (
|
||||||
|
<ColouredEnumComponent
|
||||||
|
name={name}
|
||||||
|
parentPath={parentPath}
|
||||||
|
docString={attribute.doc}
|
||||||
|
value={String(attribute.value)}
|
||||||
|
readOnly={attribute.readonly}
|
||||||
|
enumDict={attribute.enum}
|
||||||
|
addNotification={addNotification}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return <div key={name}>{name}</div>;
|
return <div key={name}>{name}</div>;
|
||||||
}
|
}
|
||||||
|
@ -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.image import Image
|
||||||
from pydase.components.number_slider import NumberSlider
|
from pydase.components.number_slider import NumberSlider
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"NumberSlider",
|
"NumberSlider",
|
||||||
"Image",
|
"Image",
|
||||||
|
"ColouredEnum",
|
||||||
]
|
]
|
||||||
|
61
src/pydase/components/coloured_enum.py
Normal file
61
src/pydase/components/coloured_enum.py
Normal file
@ -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
|
@ -311,8 +311,12 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
"value": running_task_info,
|
"value": running_task_info,
|
||||||
}
|
}
|
||||||
elif isinstance(value, Enum):
|
elif isinstance(value, Enum):
|
||||||
|
if type(value).__base__.__name__ == "ColouredEnum":
|
||||||
|
val_type = "ColouredEnum"
|
||||||
|
else:
|
||||||
|
val_type = "Enum"
|
||||||
result[key] = {
|
result[key] = {
|
||||||
"type": "Enum",
|
"type": val_type,
|
||||||
"value": value.name,
|
"value": value.name,
|
||||||
"enum": {
|
"enum": {
|
||||||
name: member.value
|
name: member.value
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from loguru import logger
|
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:
|
def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) -> None:
|
||||||
base_class_name = __value.__class__.__base__.__name__
|
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",
|
"asyncio.unix_events",
|
||||||
"_abc",
|
"_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"]
|
and type(__value).__name__ not in ["CallbackManager", "TaskManager", "Quantity"]
|
||||||
):
|
):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
Reference in New Issue
Block a user