Merge pull request #41 from tiqi-group/39-feat-add-customcss-option-to-pydaseserver

adds custom css option to pydase.Server
This commit is contained in:
Mose Müller 2023-10-17 17:04:30 +02:00 committed by GitHub
commit d334ec5284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 42 deletions

View File

@ -21,6 +21,7 @@
- [`NumberSlider`](#numberslider)
- [`ColouredEnum`](#colouredenum)
- [Extending with New Components](#extending-with-new-components)
- [Customizing Web Interface Style](#customizing-web-interface-style)
- [Understanding Service Persistence](#understanding-service-persistence)
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
- [Understanding Units in pydase](#understanding-units-in-pydase)
@ -32,18 +33,21 @@
## Features
<!-- no toc -->
* [Simple data service definition through class-based interface](#defining-a-dataService)
* [Integrated web interface for interactive access and control of your data service](#accessing-the-web-interface)
* [Support for `rpyc` connections, allowing for programmatic control and interaction with your service](#connecting-to-the-service-using-rpyc)
* [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
* [Saving and restoring the service state for service persistence](#understanding-service-persistence)
* [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
* [Support for units](#understanding-units-in-pydase)
- [Simple data service definition through class-based interface](#defining-a-dataService)
- [Integrated web interface for interactive access and control of your data service](#accessing-the-web-interface)
- [Support for `rpyc` connections, allowing for programmatic control and interaction with your service](#connecting-to-the-service-using-rpyc)
- [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
- [Customizable styling for the web interface through user-defined CSS](#customizing-web-interface-style)
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
- [Support for units](#understanding-units-in-pydase)
<!-- * Event-based callback functionality for real-time updates
* Support for additional servers for specific use-cases -->
- Support for additional servers for specific use-cases -->
## Installation
<!--installation-start-->
Install pydase using [`poetry`](https://python-poetry.org/):
```bash
@ -55,10 +59,13 @@ or `pip`:
```bash
pip install pydase
```
<!--installation-end-->
## Usage
<!--usage-start-->
Using `pydase` involves three main steps: defining a `DataService` subclass, running the server, and then connecting to the service either programmatically using `rpyc` or through the web interface.
### Defining a DataService
@ -159,6 +166,7 @@ print(client.voltage) # prints 5.0
```
In this example, replace `<ip_addr>` with the IP address of the machine where the service is running. After establishing a connection, you can interact with the service attributes as if they were local attributes.
<!--usage-end-->
## Understanding the Component System
@ -229,6 +237,7 @@ if __name__ == "__main__":
**Note** that defining classes within `DataService` classes is not supported (see [this issue](https://github.com/tiqi-group/pydase/issues/16)).
### Custom Components (`pydase.components`)
The custom components in `pydase` have two main parts:
- A **Python Component Class** in the backend, implementing the logic needed to set, update, and manage the component's state and data.
@ -342,6 +351,31 @@ Users can also extend the library by creating custom components. This involves d
<!-- Component User Guide End -->
## Customizing Web Interface Style
`pydase` allows you to enhance the user experience by customizing the web interface's appearance. You can apply your own styles globally across the web interface by passing a custom CSS file to the server during initialization.
Here's how you can use this feature:
1. Prepare your custom CSS file with the desired styles.
2. When initializing your server, use the `css` parameter of the `Server` class to specify the path to your custom CSS file.
```python
from pydase import Server, DataService
class Device(DataService):
# ... your service definition ...
if __name__ == "__main__":
service = MyService()
server = Server(service, css="path/to/your/custom.css").run()
```
This will apply the styles defined in `custom.css` to the web interface, allowing you to maintain branding consistency or improve visual accessibility.
Please ensure that the CSS file path is accessible from the server's running location. Relative or absolute paths can be used depending on your setup.
## Understanding Service Persistence
`pydase` allows you to easily persist the state of your service by saving it to a file. This is especially useful when you want to maintain the service's state across different runs.

View File

@ -129,6 +129,20 @@ const App = () => {
.then((response) => response.json())
.then((data: DataServiceJSON) => dispatch({ type: 'SET_DATA', data }));
// Allow the user to add a custom css file
fetch(`http://${hostname}:${port}/custom.css`)
.then((response) => {
if (response.ok) {
// If the file exists, create a link element for the custom CSS
const link = document.createElement('link');
link.href = `http://${hostname}:${port}/custom.css`;
link.type = 'text/css';
link.rel = 'stylesheet';
document.head.appendChild(link);
}
})
.catch(console.error); // Handle the error appropriately
socket.on('notify', onNotify);
socket.on('exception', onException);

View File

@ -22,7 +22,7 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
}, [props]);
return (
<div className={'listComponent'} id={parentPath.concat(name)}>
<div className={'listComponent'} id={parentPath.concat('.' + name)}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
)}

View File

@ -137,7 +137,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
// Set the cursor position after the component re-renders
const inputElement = document.getElementsByName(
parentPath.concat(name)
parentPath.concat('.' + name)
)[0] as HTMLInputElement;
if (inputElement && cursorPosition !== null) {
inputElement.setSelectionRange(cursorPosition, cursorPosition);
@ -287,7 +287,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
type="text"
value={inputString}
disabled={readOnly}
name={parentPath.concat(name)}
name={parentPath.concat('.' + name)}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}

View File

@ -55,7 +55,7 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
};
return (
<div className={'stringComponent'} id={parentPath.concat(name)}>
<div className={'stringComponent'} id={parentPath.concat('.' + name)}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
)}

View File

@ -5,6 +5,7 @@ from typing import Any, TypedDict
import socketio
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydase import DataService
@ -86,7 +87,7 @@ class WebAPI:
self.__sio = sio
self.__sio_app = socketio.ASGIApp(self.__sio)
def setup_fastapi_app(self) -> None: # noqa: CFQ004
def setup_fastapi_app(self) -> None: # noqa
app = FastAPI()
if self.enable_CORS:
@ -99,7 +100,6 @@ class WebAPI:
)
app.mount("/ws", self.__sio_app)
# @app.get("/version", include_in_schema=False)
@app.get("/version")
def version() -> str:
return __version__
@ -116,6 +116,13 @@ class WebAPI:
def service_properties() -> dict[str, Any]:
return self.service.serialize()
# exposing custom.css file provided by user
if self.css is not None:
@app.get("/custom.css")
async def styles():
return FileResponse(str(self.css))
app.mount(
"/",
StaticFiles(
@ -126,14 +133,6 @@ class WebAPI:
self.__fastapi_app = app
def add_endpoint(self, name: str) -> None:
# your endpoint creation code
pass
def get_custom_openapi(self) -> None:
# your custom openapi generation code
pass
@property
def sio(self) -> socketio.AsyncServer:
return self.__sio