mirror of
https://github.com/tiqi-group/pydase.git
synced 2026-02-13 13:58:41 +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",
|
"type": "firefox",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "react: firefox",
|
"name": "react: firefox",
|
||||||
"url": "http://localhost:3000",
|
"url": "http://localhost:5173",
|
||||||
"webRoot": "${workspaceFolder}/frontend"
|
"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.
|
- **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.
|
- **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).
|
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
|
### 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`:
|
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 {
|
export interface WebSetting {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
display: boolean;
|
display: boolean;
|
||||||
index: number;
|
displayOrder: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { GenericComponent } from "./GenericComponent";
|
|||||||
import { LevelName } from "./NotificationsComponent";
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { SerializedObject } from "../types/SerializedObject";
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
import useLocalStorage from "../hooks/useLocalStorage";
|
import useLocalStorage from "../hooks/useLocalStorage";
|
||||||
|
import useSortedEntries from "../hooks/useSortedEntries";
|
||||||
|
|
||||||
interface DataServiceProps {
|
interface DataServiceProps {
|
||||||
props: DataServiceJSON;
|
props: DataServiceJSON;
|
||||||
@@ -21,6 +22,8 @@ export const DataServiceComponent = React.memo(
|
|||||||
// Retrieve the initial state from localStorage, default to true if not found
|
// Retrieve the initial state from localStorage, default to true if not found
|
||||||
const [open, setOpen] = useLocalStorage(`dataServiceComponent-${id}-open`, true);
|
const [open, setOpen] = useLocalStorage(`dataServiceComponent-${id}-open`, true);
|
||||||
|
|
||||||
|
const sortedEntries = useSortedEntries(props);
|
||||||
|
|
||||||
if (displayName !== "") {
|
if (displayName !== "") {
|
||||||
return (
|
return (
|
||||||
<div className="component dataServiceComponent" id={id}>
|
<div className="component dataServiceComponent" id={id}>
|
||||||
@@ -30,9 +33,9 @@ export const DataServiceComponent = React.memo(
|
|||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Collapse in={open}>
|
<Collapse in={open}>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
{Object.entries(props).map(([key, value]) => (
|
{sortedEntries.map((value) => (
|
||||||
<GenericComponent
|
<GenericComponent
|
||||||
key={key}
|
key={value.full_access_path}
|
||||||
attribute={value}
|
attribute={value}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
@@ -46,9 +49,9 @@ export const DataServiceComponent = React.memo(
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="component dataServiceComponent" id={id}>
|
<div className="component dataServiceComponent" id={id}>
|
||||||
{Object.entries(props).map(([key, value]) => (
|
{sortedEntries.map((value) => (
|
||||||
<GenericComponent
|
<GenericComponent
|
||||||
key={key}
|
key={value.full_access_path}
|
||||||
attribute={value}
|
attribute={value}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { GenericComponent } from "./GenericComponent";
|
|||||||
import { LevelName } from "./NotificationsComponent";
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { SerializedObject } from "../types/SerializedObject";
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
import { useRenderCount } from "../hooks/useRenderCount";
|
import { useRenderCount } from "../hooks/useRenderCount";
|
||||||
|
import useSortedEntries from "../hooks/useSortedEntries";
|
||||||
|
|
||||||
interface DictComponentProps {
|
interface DictComponentProps {
|
||||||
value: Record<string, SerializedObject>;
|
value: Record<string, SerializedObject>;
|
||||||
@@ -14,16 +15,16 @@ interface DictComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DictComponent = React.memo((props: 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 renderCount = useRenderCount();
|
||||||
const valueArray = Object.values(value);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"listComponent"} id={id}>
|
<div className={"listComponent"} id={id}>
|
||||||
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
|
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
{valueArray.map((item) => {
|
{sortedEntries.map((item) => {
|
||||||
return (
|
return (
|
||||||
<GenericComponent
|
<GenericComponent
|
||||||
key={item.full_access_path}
|
key={item.full_access_path}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { GenericComponent } from "./GenericComponent";
|
|||||||
import { LevelName } from "./NotificationsComponent";
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { SerializedObject } from "../types/SerializedObject";
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
import { useRenderCount } from "../hooks/useRenderCount";
|
import { useRenderCount } from "../hooks/useRenderCount";
|
||||||
|
import useSortedEntries from "../hooks/useSortedEntries";
|
||||||
|
|
||||||
interface ListComponentProps {
|
interface ListComponentProps {
|
||||||
value: SerializedObject[];
|
value: SerializedObject[];
|
||||||
@@ -14,7 +15,9 @@ interface ListComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ListComponent = React.memo((props: 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();
|
const renderCount = useRenderCount();
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
|
|||||||
<div className={"listComponent"} id={id}>
|
<div className={"listComponent"} id={id}>
|
||||||
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
|
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
{value.map((item) => {
|
{sortedEntries.map((item) => {
|
||||||
return (
|
return (
|
||||||
<GenericComponent
|
<GenericComponent
|
||||||
key={item.full_access_path}
|
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="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Web site displaying a pydase UI." />
|
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-D2aktF3W.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user