mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-30 09:21:20 +01:00
Merge pull request #145 from tiqi-group/11-frontend-user-should-be-able-to-change-the-order-of-the-elements-in-the-frontend
adds support for altering component display order
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -25,7 +25,7 @@
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"name": "react: firefox",
|
||||
"url": "http://localhost:3000",
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/frontend"
|
||||
}
|
||||
]
|
||||
|
||||
50
README.md
50
README.md
@@ -910,10 +910,58 @@ Please ensure that the CSS file path is accessible from the server's running loc
|
||||
|
||||
- **Custom Display Names**: Modify the `"displayName"` value in the file to change how each component appears in the frontend.
|
||||
- **Control Component Visibility**: Utilize the `"display"` key-value pair to control whether a component is rendered in the frontend. Set the value to `true` to make the component visible or `false` to hide it.
|
||||
<!-- - **Adjustable Component Order**: The `"displayOrder"` values determine the order of components. Alter these values to rearrange the components as desired. -->
|
||||
- **Adjustable Component Order**: The `"displayOrder"` values determine the order of components. Alter these values to rearrange the components as desired. The value defaults to [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
|
||||
|
||||
The `web_settings.json` file will be stored in the directory specified by `SERVICE_CONFIG_DIR`. You can generate a `web_settings.json` file by setting the `GENERATE_WEB_SETTINGS` to `True`. For more information, see the [configuration section](#configuring-pydase-via-environment-variables).
|
||||
|
||||
For example, styling the following service
|
||||
|
||||
```python
|
||||
import pydase
|
||||
|
||||
|
||||
class Device(pydase.DataService):
|
||||
name = "My Device"
|
||||
some_float = 1.0
|
||||
some_int = 1
|
||||
|
||||
|
||||
class Service(pydase.DataService):
|
||||
device = Device()
|
||||
state = "RUNNING"
|
||||
|
||||
|
||||
service_instance = Service()
|
||||
pydase.Server(service_instance).run()
|
||||
```
|
||||
|
||||
with the following `web_settings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"device": {
|
||||
"displayName": "My Device",
|
||||
"displayOrder": 1
|
||||
},
|
||||
"device.name": {
|
||||
"display": false
|
||||
},
|
||||
"device.some_float": {
|
||||
"displayOrder": 1
|
||||
},
|
||||
"device.some_int": {
|
||||
"displayOrder": 0
|
||||
},
|
||||
"state": {
|
||||
"displayOrder": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
looks like this:
|
||||
|
||||

|
||||
|
||||
### Specifying a Custom Frontend Source
|
||||
|
||||
To further personalize your web interface, you can provide `pydase` with a custom frontend GUI. To do so, you can use the `frontend_src` keyword in the `pydase.Server`:
|
||||
|
||||
BIN
docs/images/Tailoring_frontend_component_layout.png
Normal file
BIN
docs/images/Tailoring_frontend_component_layout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -5,5 +5,5 @@ export const WebSettingsContext = createContext<Record<string, WebSetting>>({});
|
||||
export interface WebSetting {
|
||||
displayName: string;
|
||||
display: boolean;
|
||||
index: number;
|
||||
displayOrder: number;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GenericComponent } from "./GenericComponent";
|
||||
import { LevelName } from "./NotificationsComponent";
|
||||
import { SerializedObject } from "../types/SerializedObject";
|
||||
import useLocalStorage from "../hooks/useLocalStorage";
|
||||
import useSortedEntries from "../hooks/useSortedEntries";
|
||||
|
||||
interface DataServiceProps {
|
||||
props: DataServiceJSON;
|
||||
@@ -21,6 +22,8 @@ export const DataServiceComponent = React.memo(
|
||||
// Retrieve the initial state from localStorage, default to true if not found
|
||||
const [open, setOpen] = useLocalStorage(`dataServiceComponent-${id}-open`, true);
|
||||
|
||||
const sortedEntries = useSortedEntries(props);
|
||||
|
||||
if (displayName !== "") {
|
||||
return (
|
||||
<div className="component dataServiceComponent" id={id}>
|
||||
@@ -30,9 +33,9 @@ export const DataServiceComponent = React.memo(
|
||||
</Card.Header>
|
||||
<Collapse in={open}>
|
||||
<Card.Body>
|
||||
{Object.entries(props).map(([key, value]) => (
|
||||
{sortedEntries.map((value) => (
|
||||
<GenericComponent
|
||||
key={key}
|
||||
key={value.full_access_path}
|
||||
attribute={value}
|
||||
isInstantUpdate={isInstantUpdate}
|
||||
addNotification={addNotification}
|
||||
@@ -46,9 +49,9 @@ export const DataServiceComponent = React.memo(
|
||||
} else {
|
||||
return (
|
||||
<div className="component dataServiceComponent" id={id}>
|
||||
{Object.entries(props).map(([key, value]) => (
|
||||
{sortedEntries.map((value) => (
|
||||
<GenericComponent
|
||||
key={key}
|
||||
key={value.full_access_path}
|
||||
attribute={value}
|
||||
isInstantUpdate={isInstantUpdate}
|
||||
addNotification={addNotification}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GenericComponent } from "./GenericComponent";
|
||||
import { LevelName } from "./NotificationsComponent";
|
||||
import { SerializedObject } from "../types/SerializedObject";
|
||||
import { useRenderCount } from "../hooks/useRenderCount";
|
||||
import useSortedEntries from "../hooks/useSortedEntries";
|
||||
|
||||
interface DictComponentProps {
|
||||
value: Record<string, SerializedObject>;
|
||||
@@ -14,16 +15,16 @@ interface DictComponentProps {
|
||||
}
|
||||
|
||||
export const DictComponent = React.memo((props: DictComponentProps) => {
|
||||
const { value, docString, isInstantUpdate, addNotification, id } = props;
|
||||
const { docString, isInstantUpdate, addNotification, id } = props;
|
||||
|
||||
const sortedEntries = useSortedEntries(props.value);
|
||||
const renderCount = useRenderCount();
|
||||
const valueArray = Object.values(value);
|
||||
|
||||
return (
|
||||
<div className={"listComponent"} id={id}>
|
||||
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
|
||||
<DocStringComponent docString={docString} />
|
||||
{valueArray.map((item) => {
|
||||
{sortedEntries.map((item) => {
|
||||
return (
|
||||
<GenericComponent
|
||||
key={item.full_access_path}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GenericComponent } from "./GenericComponent";
|
||||
import { LevelName } from "./NotificationsComponent";
|
||||
import { SerializedObject } from "../types/SerializedObject";
|
||||
import { useRenderCount } from "../hooks/useRenderCount";
|
||||
import useSortedEntries from "../hooks/useSortedEntries";
|
||||
|
||||
interface ListComponentProps {
|
||||
value: SerializedObject[];
|
||||
@@ -14,7 +15,9 @@ interface ListComponentProps {
|
||||
}
|
||||
|
||||
export const ListComponent = React.memo((props: ListComponentProps) => {
|
||||
const { value, docString, isInstantUpdate, addNotification, id } = props;
|
||||
const { docString, isInstantUpdate, addNotification, id } = props;
|
||||
|
||||
const sortedEntries = useSortedEntries(props.value);
|
||||
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
@@ -22,7 +25,7 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
|
||||
<div className={"listComponent"} id={id}>
|
||||
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
|
||||
<DocStringComponent docString={docString} />
|
||||
{value.map((item) => {
|
||||
{sortedEntries.map((item) => {
|
||||
return (
|
||||
<GenericComponent
|
||||
key={item.full_access_path}
|
||||
|
||||
28
frontend/src/hooks/useSortedEntries.ts
Normal file
28
frontend/src/hooks/useSortedEntries.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useContext } from "react";
|
||||
import { WebSettingsContext } from "../WebSettings";
|
||||
import { SerializedObject } from "../types/SerializedObject";
|
||||
|
||||
export default function useSortedEntries(
|
||||
props: Record<string, SerializedObject> | SerializedObject[],
|
||||
) {
|
||||
const webSettings = useContext(WebSettingsContext);
|
||||
|
||||
// Get the order for sorting
|
||||
const getOrder = (fullAccessPath: string) => {
|
||||
return webSettings[fullAccessPath]?.displayOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
// Sort entries based on whether props is an array or an object
|
||||
let sortedEntries;
|
||||
if (Array.isArray(props)) {
|
||||
// Need to make copy of array to leave the original array unmodified
|
||||
sortedEntries = [...props].sort((objectA, objectB) => {
|
||||
return getOrder(objectA.full_access_path) - getOrder(objectB.full_access_path);
|
||||
});
|
||||
} else {
|
||||
sortedEntries = Object.values(props).sort((objectA, objectB) => {
|
||||
return getOrder(objectA.full_access_path) - getOrder(objectB.full_access_path);
|
||||
});
|
||||
}
|
||||
return sortedEntries;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site displaying a pydase UI." />
|
||||
<script type="module" crossorigin src="/assets/index-Di_tc01d.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D7tStNHJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D2aktF3W.css">
|
||||
</head>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user