feat: moving from react-create-app to vite

- loads of type fixes
- configuration changes
This commit is contained in:
Mose Müller 2024-07-04 16:45:00 +02:00
parent c0734d58ce
commit 73a3283a7d
39 changed files with 20913 additions and 22506 deletions

View File

@ -8,9 +8,12 @@
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
"prettier",
"plugin:react/recommended"
],
"rules": {
"prettier/prettier": "error"
"prettier/prettier": "error",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off"
}
}

38
frontend/.gitignore vendored
View File

@ -1,20 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,11 +1,9 @@
{
"arrowParens": "always",
"bracketSameLine": true,
"endOfLine": "auto",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"vueIndentScriptAndStyle": true,
"printWidth": 88,
"trailingComma": "none"
"arrowParens": "always",
"bracketSameLine": true,
"endOfLine": "auto",
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"printWidth": 88
}

View File

@ -1,70 +1,30 @@
# Getting Started with Create React App
# React + TypeScript + Vite
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Available Scripts
Currently, two official plugins are available:
In the project directory, you can run:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
### `npm start`
## Expanding the ESLint configuration
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
The page will reload when you make changes.\
You may also see any lint errors in the console.
- Configure the top-level `parserOptions` property like this:
### `npm test`
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

17
frontend/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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." />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

41798
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +1,43 @@
{
"name": "pydase",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fsouza/prettierd": "^0.25.1",
"@mui/material": "^5.14.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.3.0",
"react": "^18.2.0",
"react-bootstrap": "^2.8.0",
"react-bootstrap-icons": "^1.10.3",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"socket.io-client": "^4.7.1",
"web-vitals": "^3.4.0"
},
"scripts": {
"start": "NODE_ENV=development react-scripts start",
"build": "BUILD_PATH='../src/pydase/frontend' react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.9.0",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.0.3",
"typescript": "^4.9.0"
}
"name": "pydase",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build --emptyOutDir",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.14.1",
"bootstrap": "^5.3.3",
"react": "^18.3.1",
"react-bootstrap": "^2.10.0",
"react-bootstrap-icons": "^1.11.4",
"react-dom": "^18.3.1",
"react-scripts": "^5.0.1",
"socket.io-client": "^4.7.1",
"web-vitals": "^4.2.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vite": "^5.3.1"
},
"overrides": {
"typescript": "^5.2.2"
}
}

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site displaying a pydase UI."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>pydase App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,43 +1,49 @@
import { useCallback, useEffect, useReducer, useState } from 'react';
import { Navbar, Form, Offcanvas, Container } from 'react-bootstrap';
import { hostname, port, socket } from './socket';
import './App.css';
import { useCallback, useEffect, useReducer, useState } from "react";
import { Navbar, Form, Offcanvas, Container } from "react-bootstrap";
import { hostname, port, socket } from "./socket";
import "./App.css";
import {
Notifications,
Notification,
LevelName
} from './components/NotificationsComponent';
import { ConnectionToast } from './components/ConnectionToast';
import { setNestedValueByPath, State } from './utils/stateUtils';
import { WebSettingsContext, WebSetting } from './WebSettings';
import { SerializedValue, GenericComponent } from './components/GenericComponent';
LevelName,
} from "./components/NotificationsComponent";
import { ConnectionToast } from "./components/ConnectionToast";
import { setNestedValueByPath, State } from "./utils/stateUtils";
import { WebSettingsContext, WebSetting } from "./WebSettings";
import { GenericComponent } from "./components/GenericComponent";
import { SerializedObject } from "./types/SerializedObject";
type Action =
| { type: 'SET_DATA'; data: State }
| { type: "SET_DATA"; data: State }
| {
type: 'UPDATE_ATTRIBUTE';
type: "UPDATE_ATTRIBUTE";
fullAccessPath: string;
newValue: SerializedValue;
newValue: SerializedObject;
};
type UpdateMessage = {
data: { full_access_path: string; value: SerializedValue };
data: { full_access_path: string; value: SerializedObject };
};
type LogMessage = {
levelname: LevelName;
message: string;
};
const reducer = (state: State, action: Action): State => {
const reducer = (state: State | null, action: Action): State | null => {
switch (action.type) {
case 'SET_DATA':
case "SET_DATA":
return action.data;
case 'UPDATE_ATTRIBUTE': {
case "UPDATE_ATTRIBUTE": {
if (state === null) {
return null;
}
return {
...state,
value: setNestedValueByPath(state.value, action.fullAccessPath, action.newValue)
value: setNestedValueByPath(
/* @ts-expect-error state is not null here... */
state.value,
action.fullAccessPath,
action.newValue,
),
};
}
default:
@ -46,19 +52,19 @@ const reducer = (state: State, action: Action): State => {
};
const App = () => {
const [state, dispatch] = useReducer(reducer, null);
const [serviceName, setServiceName] = useState(null);
const [serviceName, setServiceName] = useState<string | null>(null);
const [webSettings, setWebSettings] = useState<Record<string, WebSetting>>({});
const [isInstantUpdate, setIsInstantUpdate] = useState(() => {
const saved = localStorage.getItem('isInstantUpdate');
const saved = localStorage.getItem("isInstantUpdate");
return saved !== null ? JSON.parse(saved) : false;
});
const [showSettings, setShowSettings] = useState(false);
const [showNotification, setShowNotification] = useState(() => {
const saved = localStorage.getItem('showNotification');
const saved = localStorage.getItem("showNotification");
return saved !== null ? JSON.parse(saved) : false;
});
const [notifications, setNotifications] = useState<Notification[]>([]);
const [connectionStatus, setConnectionStatus] = useState('connecting');
const [connectionStatus, setConnectionStatus] = useState("connecting");
useEffect(() => {
// Allow the user to add a custom css file
@ -66,21 +72,21 @@ const App = () => {
.then((response) => {
if (response.ok) {
// If the file exists, create a link element for the custom CSS
const link = document.createElement('link');
const link = document.createElement("link");
link.href = `http://${hostname}:${port}/custom.css`;
link.type = 'text/css';
link.rel = 'stylesheet';
link.type = "text/css";
link.rel = "stylesheet";
document.head.appendChild(link);
}
})
.catch(console.error); // Handle the error appropriately
socket.on('connect', () => {
socket.on("connect", () => {
// Fetch data from the API when the client connects
fetch(`http://${hostname}:${port}/service-properties`)
.then((response) => response.json())
.then((data: State) => {
dispatch({ type: 'SET_DATA', data });
dispatch({ type: "SET_DATA", data });
setServiceName(data.name);
document.title = data.name; // Setting browser tab title
@ -88,40 +94,40 @@ const App = () => {
fetch(`http://${hostname}:${port}/web-settings`)
.then((response) => response.json())
.then((data: Record<string, WebSetting>) => setWebSettings(data));
setConnectionStatus('connected');
setConnectionStatus("connected");
});
socket.on('disconnect', () => {
setConnectionStatus('disconnected');
socket.on("disconnect", () => {
setConnectionStatus("disconnected");
setTimeout(() => {
// Only set "reconnecting" is the state is still "disconnected"
// E.g. when the client has already reconnected
setConnectionStatus((currentState) =>
currentState === 'disconnected' ? 'reconnecting' : currentState
currentState === "disconnected" ? "reconnecting" : currentState,
);
}, 2000);
});
socket.on('notify', onNotify);
socket.on('log', onLogMessage);
socket.on("notify", onNotify);
socket.on("log", onLogMessage);
return () => {
socket.off('notify', onNotify);
socket.off('log', onLogMessage);
socket.off("notify", onNotify);
socket.off("log", onLogMessage);
};
}, []);
// Persist isInstantUpdate and showNotification state changes to localStorage
useEffect(() => {
localStorage.setItem('isInstantUpdate', JSON.stringify(isInstantUpdate));
localStorage.setItem("isInstantUpdate", JSON.stringify(isInstantUpdate));
}, [isInstantUpdate]);
useEffect(() => {
localStorage.setItem('showNotification', JSON.stringify(showNotification));
localStorage.setItem("showNotification", JSON.stringify(showNotification));
}, [showNotification]);
// Adding useCallback to prevent notify to change causing a re-render of all
// components
const addNotification = useCallback(
(message: string, levelname: LevelName = 'DEBUG') => {
(message: string, levelname: LevelName = "DEBUG") => {
// Getting the current time in the required format
const timeStamp = new Date().toISOString().substring(11, 19);
// Adding an id to the notification to provide a way of removing it
@ -130,15 +136,15 @@ const App = () => {
// Custom logic for notifications
setNotifications((prevNotifications) => [
{ levelname, id, message, timeStamp },
...prevNotifications
...prevNotifications,
]);
},
[]
[],
);
const removeNotificationById = (id: number) => {
setNotifications((prevNotifications) =>
prevNotifications.filter((n) => n.id !== id)
prevNotifications.filter((n) => n.id !== id),
);
};
@ -151,9 +157,9 @@ const App = () => {
// Dispatching the update to the reducer
dispatch({
type: 'UPDATE_ATTRIBUTE',
type: "UPDATE_ATTRIBUTE",
fullAccessPath,
newValue
newValue,
});
}
@ -208,7 +214,7 @@ const App = () => {
<div className="App navbarOffset">
<WebSettingsContext.Provider value={webSettings}>
<GenericComponent
attribute={state as SerializedValue}
attribute={state as SerializedObject}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>

View File

@ -1,4 +1,4 @@
import { createContext } from 'react';
import { createContext } from "react";
export const WebSettingsContext = createContext<Record<string, WebSetting>>({});

View File

@ -1,13 +1,13 @@
import React, { useEffect, useRef, useState } from 'react';
import { runMethod } from '../socket';
import { Form, Button, InputGroup, Spinner } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useRef, useState } from "react";
import { runMethod } from "../socket";
import { Form, Button, InputGroup, Spinner } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { LevelName } from "./NotificationsComponent";
type AsyncMethodProps = {
fullAccessPath: string;
value: 'RUNNING' | null;
docString?: string;
value: "RUNNING" | null;
docString: string | null;
hideOutput?: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
@ -22,7 +22,7 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
value: runningTask,
addNotification,
displayName,
id
id,
} = props;
// Conditional rendering based on the 'render' prop.
@ -33,7 +33,7 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
const renderCount = useRef(0);
const formRef = useRef(null);
const [spinning, setSpinning] = useState(false);
const name = fullAccessPath.split('.').at(-1);
const name = fullAccessPath.split(".").at(-1)!;
const parentPath = fullAccessPath.slice(0, -(name.length + 1));
useEffect(() => {
@ -59,14 +59,14 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
method_name = `start_${name}`;
}
const accessPath = [parentPath, method_name].filter((element) => element).join('.');
const accessPath = [parentPath, method_name].filter((element) => element).join(".");
setSpinning(true);
runMethod(accessPath);
};
return (
<div className="component asyncMethodComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
<Form onSubmit={execute} ref={formRef}>
@ -78,10 +78,10 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
<Button id={`button-${id}`} type="submit">
{spinning ? (
<Spinner size="sm" role="status" aria-hidden="true" />
) : runningTask === 'RUNNING' ? (
'Stop '
) : runningTask === "RUNNING" ? (
"Stop "
) : (
'Start '
"Start "
)}
</Button>
</InputGroup>
@ -89,3 +89,5 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
</div>
);
});
AsyncMethodComponent.displayName = "AsyncMethodComponent";

View File

@ -1,17 +1,17 @@
import React, { useEffect, useRef } from 'react';
import { ToggleButton } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { SerializedValue } from './GenericComponent';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useRef } from "react";
import { ToggleButton } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
type ButtonComponentProps = {
fullAccessPath: string;
value: boolean;
readOnly: boolean;
docString: string;
docString: string | null;
mapping?: [string, string]; // Enforce a tuple of two strings
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
displayName: string;
id: string;
};
@ -25,7 +25,7 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
addNotification,
changeCallback = () => {},
displayName,
id
id,
} = props;
// const buttonName = props.mapping ? (value ? props.mapping[0] : props.mapping[1]) : name;
@ -41,24 +41,24 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
const setChecked = (checked: boolean) => {
changeCallback({
type: 'bool',
type: "bool",
value: checked,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString
doc: docString,
});
};
return (
<div className={'component buttonComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div className={"component buttonComponent"} id={id}>
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
<ToggleButton
id={`toggle-check-${id}`}
type="checkbox"
variant={value ? 'success' : 'secondary'}
variant={value ? "success" : "secondary"}
checked={value}
value={displayName}
disabled={readOnly}
@ -69,3 +69,5 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
</div>
);
});
ButtonComponent.displayName = "ButtonComponent";

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Toast, Button, ToastContainer } from 'react-bootstrap';
import React, { useEffect, useState } from "react";
import { Toast, Button, ToastContainer } from "react-bootstrap";
type ConnectionToastProps = {
connectionStatus: string;
@ -36,31 +36,31 @@ export const ConnectionToast = React.memo(
delay: number | undefined;
} => {
switch (connectionStatus) {
case 'connecting':
case "connecting":
return {
message: 'Connecting...',
bg: 'info',
delay: undefined
message: "Connecting...",
bg: "info",
delay: undefined,
};
case 'connected':
return { message: 'Connected', bg: 'success', delay: 1000 };
case 'disconnected':
case "connected":
return { message: "Connected", bg: "success", delay: 1000 };
case "disconnected":
return {
message: 'Disconnected',
bg: 'danger',
delay: undefined
message: "Disconnected",
bg: "danger",
delay: undefined,
};
case 'reconnecting':
case "reconnecting":
return {
message: 'Reconnecting...',
bg: 'info',
delay: undefined
message: "Reconnecting...",
bg: "info",
delay: undefined,
};
default:
return {
message: '',
bg: 'info',
delay: undefined
message: "",
bg: "info",
delay: undefined,
};
}
};
@ -82,5 +82,7 @@ export const ConnectionToast = React.memo(
</Toast>
</ToastContainer>
);
}
},
);
ConnectionToast.displayName = "ConnectionToast";

View File

@ -1,9 +1,10 @@
import { useEffect, useState } from 'react';
import React from 'react';
import { Card, Collapse } from 'react-bootstrap';
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { SerializedValue, GenericComponent } from './GenericComponent';
import { LevelName } from './NotificationsComponent';
import { useEffect, useState } from "react";
import React from "react";
import { Card, Collapse } from "react-bootstrap";
import { ChevronDown, ChevronRight } from "react-bootstrap-icons";
import { GenericComponent } from "./GenericComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
type DataServiceProps = {
props: DataServiceJSON;
@ -13,7 +14,7 @@ type DataServiceProps = {
id: string;
};
export type DataServiceJSON = Record<string, SerializedValue>;
export type DataServiceJSON = Record<string, SerializedObject>;
export const DataServiceComponent = React.memo(
({ props, isInstantUpdate, addNotification, displayName, id }: DataServiceProps) => {
@ -28,11 +29,11 @@ export const DataServiceComponent = React.memo(
localStorage.setItem(`dataServiceComponent-${id}-open`, JSON.stringify(open));
}, [open]);
if (displayName !== '') {
if (displayName !== "") {
return (
<div className="component dataServiceComponent" id={id}>
<Card>
<Card.Header onClick={() => setOpen(!open)} style={{ cursor: 'pointer' }}>
<Card.Header onClick={() => setOpen(!open)} style={{ cursor: "pointer" }}>
{displayName} {open ? <ChevronDown /> : <ChevronRight />}
</Card.Header>
<Collapse in={open}>
@ -64,5 +65,7 @@ export const DataServiceComponent = React.memo(
</div>
);
}
}
},
);
DataServiceComponent.displayName = "DataServiceComponent";

View File

@ -1,7 +1,7 @@
import React from 'react';
import { LevelName } from './NotificationsComponent';
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
import { MethodComponent } from './MethodComponent';
import React from "react";
import { LevelName } from "./NotificationsComponent";
import { DataServiceComponent, DataServiceJSON } from "./DataServiceComponent";
import { MethodComponent } from "./MethodComponent";
type DeviceConnectionProps = {
fullAccessPath: string;
@ -19,7 +19,7 @@ export const DeviceConnectionComponent = React.memo(
isInstantUpdate,
addNotification,
displayName,
id
id,
}: DeviceConnectionProps) => {
const { connected, connect, ...updatedProps } = props;
const connectedVal = connected.value;
@ -29,14 +29,14 @@ export const DeviceConnectionComponent = React.memo(
{!connectedVal && (
<div className="overlayContent">
<div>
{displayName != '' ? displayName : 'Device'} is currently not available!
{displayName != "" ? displayName : "Device"} is currently not available!
</div>
<MethodComponent
fullAccessPath={`${fullAccessPath}.connect`}
docString={connect.doc}
addNotification={addNotification}
displayName={'reconnect'}
id={id + '-connect'}
displayName={"reconnect"}
id={id + "-connect"}
render={true}
/>
</div>
@ -50,5 +50,7 @@ export const DeviceConnectionComponent = React.memo(
/>
</div>
);
}
},
);
DeviceConnectionComponent.displayName = "DeviceConnectionComponent";

View File

@ -1,11 +1,12 @@
import React, { useEffect, useRef } from 'react';
import { DocStringComponent } from './DocStringComponent';
import { SerializedValue, GenericComponent } from './GenericComponent';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useRef } from "react";
import { DocStringComponent } from "./DocStringComponent";
import { GenericComponent } from "./GenericComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
type DictComponentProps = {
value: Record<string, SerializedValue>;
docString: string;
value: Record<string, SerializedObject>;
docString: string | null;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
id: string;
@ -22,8 +23,8 @@ export const DictComponent = React.memo((props: DictComponentProps) => {
}, [props]);
return (
<div className={'listComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div className={"listComponent"} id={id}>
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
@ -40,3 +41,5 @@ export const DictComponent = React.memo((props: DictComponentProps) => {
</div>
);
});
DictComponent.displayName = "DictComponent";

View File

@ -1,8 +1,8 @@
import { Badge, Tooltip, OverlayTrigger } from 'react-bootstrap';
import React from 'react';
import { Badge, Tooltip, OverlayTrigger } from "react-bootstrap";
import React from "react";
type DocStringProps = {
docString?: string;
docString?: string | null;
};
export const DocStringComponent = React.memo((props: DocStringProps) => {
@ -21,3 +21,5 @@ export const DocStringComponent = React.memo((props: DocStringProps) => {
</OverlayTrigger>
);
});
DocStringComponent.displayName = "DocStringComponent";

View File

@ -1,16 +1,16 @@
import React, { useEffect, useRef, useState } from 'react';
import { InputGroup, Form, Row, Col } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { SerializedValue } from './GenericComponent';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useRef, useState } from "react";
import { InputGroup, Form, Row, Col } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
export type EnumSerialization = {
type: 'Enum' | 'ColouredEnum';
type: "Enum" | "ColouredEnum";
full_access_path: string;
name: string;
value: string;
readonly: boolean;
doc?: string | null;
doc: string | null;
enum: Record<string, string>;
};
@ -19,7 +19,7 @@ type EnumComponentProps = {
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
id: string;
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
};
export const EnumComponent = React.memo((props: EnumComponentProps) => {
@ -29,12 +29,12 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
value,
doc: docString,
enum: enumDict,
readonly: readOnly
readonly: readOnly,
} = attribute;
let { changeCallback } = props;
if (changeCallback === undefined) {
changeCallback = (value: SerializedValue) => {
changeCallback = (value: SerializedObject) => {
setEnumValue(() => {
return String(value.value);
});
@ -55,8 +55,8 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
}, [value]);
return (
<div className={'component enumComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div className={"component enumComponent"} id={id}>
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
<Row>
@ -70,11 +70,11 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
// Display the Form.Control when readOnly is true
<Form.Control
style={
attribute.type == 'ColouredEnum'
attribute.type == "ColouredEnum"
? { backgroundColor: enumDict[enumValue] }
: {}
}
value={attribute.type == 'ColouredEnum' ? enumValue : enumDict[enumValue]}
value={attribute.type == "ColouredEnum" ? enumValue : enumDict[enumValue]}
name={fullAccessPath}
disabled={true}
/>
@ -85,7 +85,7 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
value={enumValue}
name={fullAccessPath}
style={
attribute.type == 'ColouredEnum'
attribute.type == "ColouredEnum"
? { backgroundColor: enumDict[enumValue] }
: {}
}
@ -97,12 +97,12 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
value: event.target.value,
full_access_path: fullAccessPath,
readonly: attribute.readonly,
doc: attribute.doc
doc: attribute.doc,
})
}>
{Object.entries(enumDict).map(([key, val]) => (
<option key={key} value={key}>
{attribute.type == 'ColouredEnum' ? key : val}
{attribute.type == "ColouredEnum" ? key : val}
</option>
))}
</Form.Select>
@ -112,3 +112,5 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
</div>
);
});
EnumComponent.displayName = "EnumComponent";

View File

@ -1,62 +1,34 @@
import React, { useContext } from 'react';
import { ButtonComponent } from './ButtonComponent';
import { NumberComponent } from './NumberComponent';
import { SliderComponent } from './SliderComponent';
import { EnumComponent, EnumSerialization } from './EnumComponent';
import { MethodComponent } from './MethodComponent';
import { AsyncMethodComponent } from './AsyncMethodComponent';
import { StringComponent } from './StringComponent';
import { ListComponent } from './ListComponent';
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
import { DeviceConnectionComponent } from './DeviceConnection';
import { ImageComponent } from './ImageComponent';
import { LevelName } from './NotificationsComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { WebSettingsContext } from '../WebSettings';
import { updateValue } from '../socket';
import { DictComponent } from './DictComponent';
import { parseFullAccessPath } from '../utils/stateUtils';
import React, { useContext } from "react";
import { ButtonComponent } from "./ButtonComponent";
import { NumberComponent, NumberObject } from "./NumberComponent";
import { SliderComponent } from "./SliderComponent";
import { EnumComponent, EnumSerialization } from "./EnumComponent";
import { MethodComponent } from "./MethodComponent";
import { AsyncMethodComponent } from "./AsyncMethodComponent";
import { StringComponent } from "./StringComponent";
import { ListComponent } from "./ListComponent";
import { DataServiceComponent, DataServiceJSON } from "./DataServiceComponent";
import { DeviceConnectionComponent } from "./DeviceConnection";
import { ImageComponent } from "./ImageComponent";
import { LevelName } from "./NotificationsComponent";
import { getIdFromFullAccessPath } from "../utils/stringUtils";
import { WebSettingsContext } from "../WebSettings";
import { updateValue } from "../socket";
import { DictComponent } from "./DictComponent";
import { parseFullAccessPath } from "../utils/stateUtils";
import { SerializedObject } from "../types/SerializedObject";
type AttributeType =
| 'str'
| 'bool'
| 'float'
| 'int'
| 'Quantity'
| 'None'
| 'list'
| 'dict'
| 'method'
| 'DataService'
| 'DeviceConnection'
| 'Enum'
| 'NumberSlider'
| 'Image'
| 'ColouredEnum';
type ValueType = boolean | string | number | Record<string, unknown>;
export type SerializedValue = {
type: AttributeType;
full_access_path: string;
name?: string;
value?: ValueType | ValueType[];
readonly: boolean;
doc?: string | null;
async?: boolean;
frontend_render?: boolean;
enum?: Record<string, string>;
};
type GenericComponentProps = {
attribute: SerializedValue;
attribute: SerializedObject;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
};
const getPathFromPathParts = (pathParts: string[]): string => {
let path = '';
let path = "";
for (const pathPart of pathParts) {
if (!pathPart.startsWith('[') && path !== '') {
path += '.';
if (!pathPart.startsWith("[") && path !== "") {
path += ".";
}
path += pathPart;
}
@ -69,7 +41,7 @@ const createDisplayNameFromAccessPath = (fullAccessPath: string): string => {
for (let i = parsedFullAccessPath.length - 1; i >= 0; i--) {
const item = parsedFullAccessPath[i];
displayNameParts.unshift(item);
if (!item.startsWith('[')) {
if (!item.startsWith("[")) {
break;
}
}
@ -94,13 +66,13 @@ export const GenericComponent = React.memo(
}
function changeCallback(
value: SerializedValue,
callback: (ack: unknown) => void = undefined
value: SerializedObject,
callback: (ack: unknown) => void = () => {},
) {
updateValue(value, callback);
}
if (attribute.type === 'bool') {
if (attribute.type === "bool") {
return (
<ButtonComponent
fullAccessPath={fullAccessPath}
@ -113,7 +85,7 @@ export const GenericComponent = React.memo(
id={id}
/>
);
} else if (attribute.type === 'float' || attribute.type === 'int') {
} else if (attribute.type === "float" || attribute.type === "int") {
return (
<NumberComponent
type={attribute.type}
@ -128,15 +100,15 @@ export const GenericComponent = React.memo(
id={id}
/>
);
} else if (attribute.type === 'Quantity') {
} else if (attribute.type === "Quantity") {
return (
<NumberComponent
type="Quantity"
fullAccessPath={fullAccessPath}
docString={attribute.doc}
readOnly={attribute.readonly}
value={Number(attribute.value['magnitude'])}
unit={attribute.value['unit']}
value={Number(attribute.value["magnitude"])}
unit={attribute.value["unit"]}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
changeCallback={changeCallback}
@ -144,16 +116,16 @@ export const GenericComponent = React.memo(
id={id}
/>
);
} else if (attribute.type === 'NumberSlider') {
} else if (attribute.type === "NumberSlider") {
return (
<SliderComponent
fullAccessPath={fullAccessPath}
docString={attribute.value['value'].doc}
docString={attribute.value["value"].doc}
readOnly={attribute.readonly}
value={attribute.value['value']}
min={attribute.value['min']}
max={attribute.value['max']}
stepSize={attribute.value['step_size']}
value={attribute.value["value"] as NumberObject}
min={attribute.value["min"] as NumberObject}
max={attribute.value["max"] as NumberObject}
stepSize={attribute.value["step_size"] as NumberObject}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
changeCallback={changeCallback}
@ -161,7 +133,7 @@ export const GenericComponent = React.memo(
id={id}
/>
);
} else if (attribute.type === 'Enum' || attribute.type === 'ColouredEnum') {
} else if (attribute.type === "Enum" || attribute.type === "ColouredEnum") {
return (
<EnumComponent
attribute={attribute as EnumSerialization}
@ -171,7 +143,7 @@ export const GenericComponent = React.memo(
id={id}
/>
);
} else if (attribute.type === 'method') {
} else if (attribute.type === "method") {
if (!attribute.async) {
return (
<MethodComponent
@ -188,7 +160,7 @@ export const GenericComponent = React.memo(
<AsyncMethodComponent
fullAccessPath={fullAccessPath}
docString={attribute.doc}
value={attribute.value as 'RUNNING' | null}
value={attribute.value as "RUNNING" | null}
addNotification={addNotification}
displayName={displayName}
id={id}
@ -196,7 +168,7 @@ export const GenericComponent = React.memo(
/>
);
}
} else if (attribute.type === 'str') {
} else if (attribute.type === "str") {
return (
<StringComponent
fullAccessPath={fullAccessPath}
@ -210,7 +182,7 @@ export const GenericComponent = React.memo(
id={id}
/>
);
} else if (attribute.type === 'DataService') {
} else if (attribute.type === "DataService") {
return (
<DataServiceComponent
props={attribute.value as DataServiceJSON}
@ -220,7 +192,7 @@ export const GenericComponent = React.memo(
id={id}
/>
);
} else if (attribute.type === 'DeviceConnection') {
} else if (attribute.type === "DeviceConnection") {
return (
<DeviceConnectionComponent
fullAccessPath={fullAccessPath}
@ -231,41 +203,42 @@ export const GenericComponent = React.memo(
id={id}
/>
);
} else if (attribute.type === 'list') {
} else if (attribute.type === "list") {
return (
<ListComponent
value={attribute.value as SerializedValue[]}
value={attribute.value}
docString={attribute.doc}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
id={id}
/>
);
} else if (attribute.type === 'dict') {
} else if (attribute.type === "dict") {
return (
<DictComponent
value={attribute.value as Record<string, SerializedValue>}
value={attribute.value}
docString={attribute.doc}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
id={id}
/>
);
} else if (attribute.type === 'Image') {
} else if (attribute.type === "Image") {
return (
<ImageComponent
fullAccessPath={fullAccessPath}
docString={attribute.value['value'].doc}
docString={attribute.value["value"].doc}
displayName={displayName}
id={id}
addNotification={addNotification}
// Add any other specific props for the ImageComponent here
value={attribute.value['value']['value'] as string}
format={attribute.value['format']['value'] as string}
value={attribute.value["value"]["value"] as string}
format={attribute.value["format"]["value"] as string}
/>
);
} else {
return <div key={fullAccessPath}>{fullAccessPath}</div>;
}
}
},
);
GenericComponent.displayName = "GenericComponent";

View File

@ -1,13 +1,13 @@
import React, { useEffect, useRef, useState } from 'react';
import { Card, Collapse, Image } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useRef, useState } from "react";
import { Card, Collapse, Image } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { ChevronDown, ChevronRight } from "react-bootstrap-icons";
import { LevelName } from "./NotificationsComponent";
type ImageComponentProps = {
fullAccessPath: string;
value: string;
docString: string;
docString: string | null;
format: string;
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
@ -34,7 +34,7 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
<Card>
<Card.Header
onClick={() => setOpen(!open)}
style={{ cursor: 'pointer' }} // Change cursor style on hover
style={{ cursor: "pointer" }} // Change cursor style on hover
>
{displayName}
<DocStringComponent docString={docString} />
@ -42,10 +42,10 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
</Card.Header>
<Collapse in={open}>
<Card.Body>
{process.env.NODE_ENV === 'development' && (
{process.env.NODE_ENV === "development" && (
<p>Render count: {renderCount.current}</p>
)}
{format === '' && value === '' ? (
{format === "" && value === "" ? (
<p>No image set in the backend.</p>
) : (
<Image src={`data:image/${format.toLowerCase()};base64,${value}`}></Image>
@ -56,3 +56,5 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
</div>
);
});
ImageComponent.displayName = "ImageComponent";

View File

@ -1,11 +1,12 @@
import React, { useEffect, useRef } from 'react';
import { DocStringComponent } from './DocStringComponent';
import { SerializedValue, GenericComponent } from './GenericComponent';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useRef } from "react";
import { DocStringComponent } from "./DocStringComponent";
import { GenericComponent } from "./GenericComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
type ListComponentProps = {
value: SerializedValue[];
docString: string;
value: SerializedObject[];
docString: string | null;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
id: string;
@ -21,8 +22,8 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
}, [props]);
return (
<div className={'listComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div className={"listComponent"} id={id}>
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
@ -39,3 +40,5 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
</div>
);
});
ListComponent.displayName = "ListComponent";

View File

@ -1,12 +1,12 @@
import React, { useEffect, useRef } from 'react';
import { runMethod } from '../socket';
import { Button, Form } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useRef } from "react";
import { runMethod } from "../socket";
import { Button, Form } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { LevelName } from "./NotificationsComponent";
type MethodProps = {
fullAccessPath: string;
docString?: string;
docString: string | null;
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
id: string;
@ -43,7 +43,7 @@ export const MethodComponent = React.memo((props: MethodProps) => {
return (
<div className="component methodComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
<Form onSubmit={execute} ref={formRef}>
@ -55,3 +55,5 @@ export const MethodComponent = React.memo((props: MethodProps) => {
</div>
);
});
MethodComponent.displayName = "MethodComponent";

View File

@ -1,7 +1,7 @@
import React from 'react';
import { ToastContainer, Toast } from 'react-bootstrap';
import React from "react";
import { ToastContainer, Toast } from "react-bootstrap";
export type LevelName = 'CRITICAL' | 'ERROR' | 'WARNING' | 'INFO' | 'DEBUG';
export type LevelName = "CRITICAL" | "ERROR" | "WARNING" | "INFO" | "DEBUG";
export type Notification = {
id: number;
timeStamp: string;
@ -23,10 +23,10 @@ export const Notifications = React.memo((props: NotificationProps) => {
{notifications.map((notification) => {
// Determine if the toast should be shown
const shouldShow =
notification.levelname === 'ERROR' ||
notification.levelname === 'CRITICAL' ||
notification.levelname === "ERROR" ||
notification.levelname === "CRITICAL" ||
(showNotification &&
['WARNING', 'INFO', 'DEBUG'].includes(notification.levelname));
["WARNING", "INFO", "DEBUG"].includes(notification.levelname));
if (!shouldShow) {
return null;
@ -34,31 +34,31 @@ export const Notifications = React.memo((props: NotificationProps) => {
return (
<Toast
className={notification.levelname.toLowerCase() + 'Toast'}
className={notification.levelname.toLowerCase() + "Toast"}
key={notification.id}
onClose={() => removeNotificationById(notification.id)}
onClick={() => removeNotificationById(notification.id)}
onMouseLeave={() => {
if (notification.levelname !== 'ERROR') {
if (notification.levelname !== "ERROR") {
removeNotificationById(notification.id);
}
}}
show={true}
autohide={
notification.levelname === 'WARNING' ||
notification.levelname === 'INFO' ||
notification.levelname === 'DEBUG'
notification.levelname === "WARNING" ||
notification.levelname === "INFO" ||
notification.levelname === "DEBUG"
}
delay={
notification.levelname === 'WARNING' ||
notification.levelname === 'INFO' ||
notification.levelname === 'DEBUG'
notification.levelname === "WARNING" ||
notification.levelname === "INFO" ||
notification.levelname === "DEBUG"
? 2000
: undefined
}>
<Toast.Header
closeButton={false}
className={notification.levelname.toLowerCase() + 'Toast text-right'}>
className={notification.levelname.toLowerCase() + "Toast text-right"}>
<strong className="me-auto">{notification.levelname}</strong>
<small>{notification.timeStamp}</small>
</Toast.Header>
@ -69,3 +69,5 @@ export const Notifications = React.memo((props: NotificationProps) => {
</ToastContainer>
);
});
Notifications.displayName = "Notifications";

View File

@ -1,45 +1,43 @@
import React, { useEffect, useState, useRef } from 'react';
import { Form, InputGroup } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import '../App.css';
import { LevelName } from './NotificationsComponent';
import { SerializedValue } from './GenericComponent';
import React, { useEffect, useState, useRef } from "react";
import { Form, InputGroup } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import "../App.css";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import { QuantityMap } from "../types/QuantityMap";
// TODO: add button functionality
export type QuantityObject = {
type: 'Quantity';
type: "Quantity";
readonly: boolean;
value: {
magnitude: number;
unit: string;
};
doc?: string;
value: QuantityMap;
doc: string | null;
};
export type IntObject = {
type: 'int';
type: "int";
readonly: boolean;
value: number;
doc?: string;
doc: string | null;
};
export type FloatObject = {
type: 'float';
type: "float";
readonly: boolean;
value: number;
doc?: string;
doc: string | null;
};
export type NumberObject = IntObject | FloatObject | QuantityObject;
type NumberComponentProps = {
type: 'float' | 'int' | 'Quantity';
type: "float" | "int" | "Quantity";
fullAccessPath: string;
value: number;
readOnly: boolean;
docString: string;
docString: string | null;
isInstantUpdate: boolean;
unit?: string;
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
displayName?: string;
id: string;
};
@ -49,11 +47,11 @@ type NumberComponentProps = {
const handleArrowKey = (
key: string,
value: string,
selectionStart: number
selectionStart: number,
// selectionEnd: number
) => {
// Split the input value into the integer part and decimal part
const parts = value.split('.');
const parts = value.split(".");
const beforeDecimalCount = parts[0].length; // Count digits before the decimal
const afterDecimalCount = parts[1] ? parts[1].length : 0; // Count digits after the decimal
@ -69,14 +67,14 @@ const handleArrowKey = (
// Convert the input value to a number, increment or decrement it based on the
// arrow key
const numValue = parseFloat(value) + (key === 'ArrowUp' ? increment : -increment);
const numValue = parseFloat(value) + (key === "ArrowUp" ? increment : -increment);
// Convert the resulting number to a string, maintaining the same number of digits
// after the decimal
const newValue = numValue.toFixed(afterDecimalCount);
// Check if the length of the integer part of the number string has in-/decreased
const newBeforeDecimalCount = newValue.split('.')[0].length;
const newBeforeDecimalCount = newValue.split(".")[0].length;
if (newBeforeDecimalCount > beforeDecimalCount) {
// Move the cursor one position to the right
selectionStart += 1;
@ -90,18 +88,18 @@ const handleArrowKey = (
const handleBackspaceKey = (
value: string,
selectionStart: number,
selectionEnd: number
selectionEnd: number,
) => {
if (selectionEnd > selectionStart) {
// If there is a selection, delete all characters in the selection
return {
value: value.slice(0, selectionStart) + value.slice(selectionEnd),
selectionStart
selectionStart,
};
} else if (selectionStart > 0) {
return {
value: value.slice(0, selectionStart - 1) + value.slice(selectionStart),
selectionStart: selectionStart - 1
selectionStart: selectionStart - 1,
};
}
return { value, selectionStart };
@ -110,18 +108,18 @@ const handleBackspaceKey = (
const handleDeleteKey = (
value: string,
selectionStart: number,
selectionEnd: number
selectionEnd: number,
) => {
if (selectionEnd > selectionStart) {
// If there is a selection, delete all characters in the selection
return {
value: value.slice(0, selectionStart) + value.slice(selectionEnd),
selectionStart
selectionStart,
};
} else if (selectionStart < value.length) {
return {
value: value.slice(0, selectionStart) + value.slice(selectionStart + 1),
selectionStart
selectionStart,
};
}
return { value, selectionStart };
@ -131,12 +129,12 @@ const handleNumericKey = (
key: string,
value: string,
selectionStart: number,
selectionEnd: number
selectionEnd: number,
) => {
// Check if a number key or a decimal point key is pressed
if (key === '.' && value.includes('.')) {
if (key === "." && value.includes(".")) {
// Check if value already contains a decimal. If so, ignore input.
console.warn('Invalid input! Ignoring...');
console.warn("Invalid input! Ignoring...");
return { value, selectionStart };
}
@ -166,98 +164,111 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
addNotification,
changeCallback = () => {},
displayName,
id
id,
} = props;
// Create a state for the cursor position
const [cursorPosition, setCursorPosition] = useState(null);
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
// Create a state for the input string
const [inputString, setInputString] = useState(value.toString());
const renderCount = useRef(0);
const handleKeyDown = (event) => {
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
const { key, target } = event;
console.log(typeof key);
// Typecast
const inputTarget = target as HTMLInputElement;
if (
key === 'F1' ||
key === 'F5' ||
key === 'F12' ||
key === 'Tab' ||
key === 'ArrowRight' ||
key === 'ArrowLeft'
key === "F1" ||
key === "F5" ||
key === "F12" ||
key === "Tab" ||
key === "ArrowRight" ||
key === "ArrowLeft"
) {
return;
}
event.preventDefault();
// Get the current input value and cursor position
const { value } = target;
let { selectionStart } = target;
const { selectionEnd } = target;
const { value } = inputTarget;
const selectionEnd = inputTarget.selectionEnd ?? 0;
let selectionStart = inputTarget.selectionStart ?? 0;
let newValue: string = value;
if (event.ctrlKey && key === 'a') {
if (event.ctrlKey && key === "a") {
// Select everything when pressing Ctrl + a
target.setSelectionRange(0, target.value.length);
inputTarget.setSelectionRange(0, value.length);
return;
} else if (key === '-') {
if (selectionStart === 0 && !value.startsWith('-')) {
newValue = '-' + value;
} else if (key === "-") {
if (selectionStart === 0 && !value.startsWith("-")) {
newValue = "-" + value;
selectionStart++;
} else if (value.startsWith('-') && selectionStart === 1) {
} else if (value.startsWith("-") && selectionStart === 1) {
newValue = value.substring(1); // remove minus sign
selectionStart--;
} else {
return; // Ignore "-" pressed in other positions
}
} else if (!isNaN(key) && key !== ' ') {
} else if (key >= "0" && key <= "9") {
// Check if a number key or a decimal point key is pressed
({ value: newValue, selectionStart } = handleNumericKey(
key,
value,
selectionStart,
selectionEnd
selectionEnd,
));
} else if (key === '.' && (type === 'float' || type === 'Quantity')) {
} else if (key === "." && (type === "float" || type === "Quantity")) {
({ value: newValue, selectionStart } = handleNumericKey(
key,
value,
selectionStart,
selectionEnd
selectionEnd,
));
} else if (key === 'ArrowUp' || key === 'ArrowDown') {
} else if (key === "ArrowUp" || key === "ArrowDown") {
({ value: newValue, selectionStart } = handleArrowKey(
key,
value,
selectionStart
selectionStart,
// selectionEnd
));
} else if (key === 'Backspace') {
} else if (key === "Backspace") {
({ value: newValue, selectionStart } = handleBackspaceKey(
value,
selectionStart,
selectionEnd
selectionEnd,
));
} else if (key === 'Delete') {
} else if (key === "Delete") {
({ value: newValue, selectionStart } = handleDeleteKey(
value,
selectionStart,
selectionEnd
selectionEnd,
));
} else if (key === 'Enter' && !isInstantUpdate) {
let updatedValue: number | Record<string, unknown> = Number(newValue);
if (type === 'Quantity') {
updatedValue = {
magnitude: Number(newValue),
unit: unit
} else if (key === "Enter" && !isInstantUpdate) {
let serializedObject: SerializedObject;
if (type === "Quantity") {
serializedObject = {
type: "Quantity",
value: {
magnitude: Number(newValue),
unit: unit,
} as QuantityMap,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
} else {
serializedObject = {
type: type,
value: Number(newValue),
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
}
changeCallback({
type: type,
value: updatedValue,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString
});
changeCallback(serializedObject);
return;
} else {
console.debug(key);
@ -266,20 +277,29 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
// Update the input value and maintain the cursor position
if (isInstantUpdate) {
let updatedValue: number | Record<string, unknown> = Number(newValue);
if (type === 'Quantity') {
updatedValue = {
magnitude: Number(newValue),
unit: unit
let serializedObject: SerializedObject;
if (type === "Quantity") {
serializedObject = {
type: "Quantity",
value: {
magnitude: Number(newValue),
unit: unit,
} as QuantityMap,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
} else {
serializedObject = {
type: type,
value: Number(newValue),
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
}
changeCallback({
type: type,
value: updatedValue,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString
});
changeCallback(serializedObject);
}
setInputString(newValue);
@ -291,26 +311,35 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
const handleBlur = () => {
if (!isInstantUpdate) {
// If not in "instant update" mode, emit an update when the input field loses focus
let updatedValue: number | Record<string, unknown> = Number(inputString);
if (type === 'Quantity') {
updatedValue = {
magnitude: Number(inputString),
unit: unit
let serializedObject: SerializedObject;
if (type === "Quantity") {
serializedObject = {
type: "Quantity",
value: {
magnitude: Number(inputString),
unit: unit,
} as QuantityMap,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
} else {
serializedObject = {
type: type,
value: Number(inputString),
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
}
changeCallback({
type: type,
value: updatedValue,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString
});
changeCallback(serializedObject);
}
};
useEffect(() => {
// Parse the input string to a number for comparison
const numericInputString =
type === 'int' ? parseInt(inputString) : parseFloat(inputString);
type === "int" ? parseInt(inputString) : parseFloat(inputString);
// Only update the inputString if it's different from the prop value
if (value !== numericInputString) {
setInputString(value.toString());
@ -319,7 +348,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
// emitting notification
let notificationMsg = `${fullAccessPath} changed to ${props.value}`;
if (unit === undefined) {
notificationMsg += '.';
notificationMsg += ".";
} else {
notificationMsg += ` ${unit}.`;
}
@ -336,7 +365,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
return (
<div className="component numberComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
<InputGroup>
@ -354,10 +383,12 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
name={id}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
className={isInstantUpdate && !readOnly ? "instantUpdate" : ""}
/>
{unit && <InputGroup.Text>{unit}</InputGroup.Text>}
</InputGroup>
</div>
);
});
NumberComponent.displayName = "NumberComponent";

View File

@ -1,10 +1,11 @@
import React, { useEffect, useRef, useState } from 'react';
import { InputGroup, Form, Row, Col, Collapse, ToggleButton } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { Slider } from '@mui/material';
import { NumberComponent, NumberObject } from './NumberComponent';
import { LevelName } from './NotificationsComponent';
import { SerializedValue } from './GenericComponent';
import React, { useEffect, useRef, useState } from "react";
import { InputGroup, Form, Row, Col, Collapse, ToggleButton } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { Slider } from "@mui/material";
import { NumberComponent, NumberObject } from "./NumberComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import { QuantityMap } from "../types/QuantityMap";
type SliderComponentProps = {
fullAccessPath: string;
@ -12,11 +13,11 @@ type SliderComponentProps = {
max: NumberObject;
value: NumberObject;
readOnly: boolean;
docString: string;
docString: string | null;
stepSize: NumberObject;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
displayName: string;
id: string;
};
@ -35,7 +36,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
addNotification,
changeCallback = () => {},
displayName,
id
id,
} = props;
useEffect(() => {
@ -58,44 +59,76 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
addNotification(`${fullAccessPath}.stepSize changed to ${stepSize.value}.`);
}, [props.stepSize]);
const handleOnChange = (event, newNumber: number | number[]) => {
const handleOnChange = (_: Event, newNumber: number | number[]) => {
// This will never be the case as we do not have a range slider. However, we should
// make sure this is properly handled.
if (Array.isArray(newNumber)) {
newNumber = newNumber[0];
}
changeCallback({
type: value.type,
value: newNumber,
full_access_path: `${fullAccessPath}.value`,
readonly: value.readonly,
doc: docString
});
let serializedObject: SerializedObject;
if (value.type === "Quantity") {
serializedObject = {
type: "Quantity",
value: {
magnitude: newNumber,
unit: value.value.unit,
} as QuantityMap,
full_access_path: `${fullAccessPath}.value`,
readonly: value.readonly,
doc: docString,
};
} else {
serializedObject = {
type: value.type,
value: newNumber,
full_access_path: `${fullAccessPath}.value`,
readonly: value.readonly,
doc: docString,
};
}
changeCallback(serializedObject);
};
const handleValueChange = (
newValue: number,
name: string,
valueObject: NumberObject
valueObject: NumberObject,
) => {
changeCallback({
type: valueObject.type,
value: newValue,
full_access_path: `${fullAccessPath}.${name}`,
readonly: valueObject.readonly
});
let serializedObject: SerializedObject;
if (valueObject.type === "Quantity") {
serializedObject = {
type: valueObject.type,
value: {
magnitude: newValue,
unit: valueObject.value.unit,
} as QuantityMap,
full_access_path: `${fullAccessPath}.${name}`,
readonly: valueObject.readonly,
doc: null,
};
} else {
serializedObject = {
type: valueObject.type,
value: newValue,
full_access_path: `${fullAccessPath}.${name}`,
readonly: valueObject.readonly,
doc: null,
};
}
changeCallback(serializedObject);
};
const deconstructNumberDict = (
numberDict: NumberObject
): [number, boolean, string | null] => {
let numberMagnitude: number;
let numberUnit: string | null = null;
numberDict: NumberObject,
): [number, boolean, string | undefined] => {
let numberMagnitude: number = 0;
let numberUnit: string | undefined = undefined;
const numberReadOnly = numberDict.readonly;
if (numberDict.type === 'int' || numberDict.type === 'float') {
if (numberDict.type === "int" || numberDict.type === "float") {
numberMagnitude = numberDict.value;
} else if (numberDict.type === 'Quantity') {
} else if (numberDict.type === "Quantity") {
numberMagnitude = numberDict.value.magnitude;
numberUnit = numberDict.value.unit;
}
@ -110,7 +143,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
return (
<div className="component sliderComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
@ -123,7 +156,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
</Col>
<Col xs="5" xl>
<Slider
style={{ margin: '0px 0px 10px 0px' }}
style={{ margin: "0px 0px 10px 0px" }}
aria-label="Always visible"
// valueLabelDisplay="on"
disabled={valueReadOnly}
@ -134,7 +167,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
step={stepSizeMagnitude}
marks={[
{ value: minMagnitude, label: `${minMagnitude}` },
{ value: maxMagnitude, label: `${maxMagnitude}` }
{ value: maxMagnitude, label: `${maxMagnitude}` },
]}
/>
</Col>
@ -144,12 +177,12 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
fullAccessPath={`${fullAccessPath}.value`}
docString={docString}
readOnly={valueReadOnly}
type="float"
type={value.type}
value={valueMagnitude}
unit={valueUnit}
addNotification={() => {}}
changeCallback={changeCallback}
id={id + '-value'}
id={id + "-value"}
/>
</Col>
<Col xs="auto">
@ -179,14 +212,14 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Group>
<Row
className="justify-content-center"
style={{ paddingTop: '20px', margin: '10px' }}>
style={{ paddingTop: "20px", margin: "10px" }}>
<Col xs="auto">
<Form.Label>Min Value</Form.Label>
<Form.Control
type="number"
value={minMagnitude}
disabled={minReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), 'min', min)}
onChange={(e) => handleValueChange(Number(e.target.value), "min", min)}
/>
</Col>
@ -196,7 +229,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
type="number"
value={maxMagnitude}
disabled={maxReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), 'max', max)}
onChange={(e) => handleValueChange(Number(e.target.value), "max", max)}
/>
</Col>
@ -207,7 +240,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
value={stepSizeMagnitude}
disabled={stepSizeReadOnly}
onChange={(e) =>
handleValueChange(Number(e.target.value), 'step_size', stepSize)
handleValueChange(Number(e.target.value), "step_size", stepSize)
}
/>
</Col>
@ -217,3 +250,5 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
</div>
);
});
SliderComponent.displayName = "SliderComponent";

View File

@ -1,9 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import { Form, InputGroup } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import '../App.css';
import { LevelName } from './NotificationsComponent';
import { SerializedValue } from './GenericComponent';
import React, { useEffect, useRef, useState } from "react";
import { Form, InputGroup } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import "../App.css";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
// TODO: add button functionality
@ -11,10 +11,10 @@ type StringComponentProps = {
fullAccessPath: string;
value: string;
readOnly: boolean;
docString: string;
docString: string | null;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
displayName: string;
id: string;
};
@ -28,7 +28,7 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
addNotification,
changeCallback = () => {},
displayName,
id
id,
} = props;
const renderCount = useRef(0);
@ -46,27 +46,27 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
addNotification(`${fullAccessPath} changed to ${props.value}.`);
}, [props.value]);
const handleChange = (event) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputString(event.target.value);
if (isInstantUpdate) {
changeCallback({
type: 'str',
type: "str",
value: event.target.value,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString
doc: docString,
});
}
};
const handleKeyDown = (event) => {
if (event.key === 'Enter' && !isInstantUpdate) {
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && !isInstantUpdate) {
changeCallback({
type: 'str',
type: "str",
value: inputString,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString
doc: docString,
});
event.preventDefault();
}
@ -75,18 +75,18 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
const handleBlur = () => {
if (!isInstantUpdate) {
changeCallback({
type: 'str',
type: "str",
value: inputString,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString
doc: docString,
});
}
};
return (
<div className="component stringComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
{process.env.NODE_ENV === "development" && (
<div>Render count: {renderCount.current}</div>
)}
<InputGroup>
@ -102,9 +102,11 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
className={isInstantUpdate && !readOnly ? "instantUpdate" : ""}
/>
</InputGroup>
</div>
);
});
StringComponent.displayName = "StringComponent";

View File

@ -1,10 +1,13 @@
import App from './App';
import { createRoot } from 'react-dom/client';
import App from "./App";
import React from "react";
import ReactDOM from "react-dom/client";
// Importing the Bootstrap CSS
import 'bootstrap/dist/css/bootstrap.min.css';
import "bootstrap/dist/css/bootstrap.min.css";
// Render the App component into the #root div
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -1,30 +1,30 @@
import { io } from 'socket.io-client';
import { SerializedValue } from './components/GenericComponent';
import { serializeDict, serializeList } from './utils/serializationUtils';
import { io } from "socket.io-client";
import { serializeDict, serializeList } from "./utils/serializationUtils";
import { SerializedObject } from "./types/SerializedObject";
export const hostname =
process.env.NODE_ENV === 'development' ? `localhost` : window.location.hostname;
process.env.NODE_ENV === "development" ? `localhost` : window.location.hostname;
export const port =
process.env.NODE_ENV === 'development' ? 8001 : window.location.port;
process.env.NODE_ENV === "development" ? 8001 : window.location.port;
const URL = `ws://${hostname}:${port}/`;
console.debug('Websocket: ', URL);
console.debug("Websocket: ", URL);
export const socket = io(URL, { path: '/ws/socket.io', transports: ['websocket'] });
export const socket = io(URL, { path: "/ws/socket.io", transports: ["websocket"] });
export const updateValue = (
serializedObject: SerializedValue,
callback?: (ack: unknown) => void
serializedObject: SerializedObject,
callback?: (ack: unknown) => void,
) => {
if (callback) {
socket.emit(
'update_value',
{ access_path: serializedObject['full_access_path'], value: serializedObject },
callback
"update_value",
{ access_path: serializedObject["full_access_path"], value: serializedObject },
callback,
);
} else {
socket.emit('update_value', {
access_path: serializedObject['full_access_path'],
value: serializedObject
socket.emit("update_value", {
access_path: serializedObject["full_access_path"],
value: serializedObject,
});
}
};
@ -33,22 +33,22 @@ export const runMethod = (
accessPath: string,
args: unknown[] = [],
kwargs: Record<string, unknown> = {},
callback?: (ack: unknown) => void
callback?: (ack: unknown) => void,
) => {
const serializedArgs = serializeList(args);
const serializedKwargs = serializeDict(kwargs);
if (callback) {
socket.emit(
'trigger_method',
"trigger_method",
{ access_path: accessPath, args: serializedArgs, kwargs: serializedKwargs },
callback
callback,
);
} else {
socket.emit('trigger_method', {
socket.emit("trigger_method", {
access_path: accessPath,
args: serializedArgs,
kwargs: serializedKwargs
kwargs: serializedKwargs,
});
}
};

View File

@ -0,0 +1,4 @@
export type QuantityMap = {
magnitude: number;
unit: string;
};

View File

@ -0,0 +1,101 @@
import { QuantityMap } from "./QuantityMap";
type SignatureDict = {
parameters: Record<string, Record<string, unknown>>;
return_annotation: Record<string, unknown>;
};
type SerializedObjectBase = {
full_access_path: string;
doc: string | null;
readonly: boolean;
};
type SerializedInteger = SerializedObjectBase & {
value: number;
type: "int";
};
type SerializedFloat = SerializedObjectBase & {
value: number;
type: "float";
};
type SerializedQuantity = SerializedObjectBase & {
value: QuantityMap;
type: "Quantity";
};
type SerializedBool = SerializedObjectBase & {
value: boolean;
type: "bool";
};
type SerializedString = SerializedObjectBase & {
value: string;
type: "str";
};
type SerializedEnum = SerializedObjectBase & {
name: string;
value: string;
type: "Enum" | "ColouredEnum";
enum: Record<string, unknown>;
};
type SerializedList = SerializedObjectBase & {
value: SerializedObject[];
type: "list";
};
type SerializedDict = SerializedObjectBase & {
value: Record<string, SerializedObject>;
type: "dict";
};
type SerializedNoneType = SerializedObjectBase & {
value: null;
type: "NoneType";
};
type SerializedNoValue = SerializedObjectBase & {
value: null;
type: "None";
};
type SerializedMethod = SerializedObjectBase & {
value: "RUNNING" | null;
type: "method";
async: boolean;
signature: SignatureDict;
frontend_render: boolean;
};
type SerializedException = SerializedObjectBase & {
name: string;
value: string;
type: "Exception";
};
type DataServiceTypes = "DataService" | "Image" | "NumberSlider" | "DeviceConnection";
type SerializedDataService = SerializedObjectBase & {
name: string;
value: Record<string, SerializedObject>;
type: DataServiceTypes;
};
export type SerializedObject =
| SerializedBool
| SerializedFloat
| SerializedInteger
| SerializedString
| SerializedList
| SerializedDict
| SerializedNoneType
| SerializedMethod
| SerializedException
| SerializedDataService
| SerializedEnum
| SerializedQuantity
| SerializedNoValue;

View File

@ -1,101 +1,100 @@
import { SerializedObject } from "../types/SerializedObject";
const serializePrimitive = (
obj: number | boolean | string | null,
accessPath: string
) => {
let type: string;
if (typeof obj === 'number') {
type = Number.isInteger(obj) ? 'int' : 'float';
accessPath: string,
): SerializedObject => {
if (typeof obj === "number") {
return {
full_access_path: accessPath,
doc: null,
readonly: false,
type,
value: obj
type: Number.isInteger(obj) ? "int" : "float",
value: obj,
};
} else if (typeof obj === 'boolean') {
type = 'bool';
} else if (typeof obj === "boolean") {
return {
full_access_path: accessPath,
doc: null,
readonly: false,
type,
value: obj
type: "bool",
value: obj,
};
} else if (typeof obj === 'string') {
type = 'str';
} else if (typeof obj === "string") {
return {
full_access_path: accessPath,
doc: null,
readonly: false,
type,
value: obj
type: "str",
value: obj,
};
} else if (obj === null) {
type = 'NoneType';
return {
full_access_path: accessPath,
doc: null,
readonly: false,
type,
value: null
type: "None",
value: null,
};
} else {
throw new Error('Unsupported type for serialization');
throw new Error("Unsupported type for serialization");
}
};
export const serializeList = (obj: unknown[], accessPath: string = '') => {
export const serializeList = (obj: unknown[], accessPath: string = "") => {
const doc = null;
const value = obj.map((item, index) => {
if (
typeof item === 'number' ||
typeof item === 'boolean' ||
typeof item === 'string' ||
typeof item === "number" ||
typeof item === "boolean" ||
typeof item === "string" ||
item === null
) {
serializePrimitive(
item as number | boolean | string | null,
`${accessPath}[${index}]`
`${accessPath}[${index}]`,
);
}
});
return {
full_access_path: accessPath,
type: 'list',
type: "list",
value,
readonly: false,
doc
doc,
};
};
export const serializeDict = (
obj: Record<string, unknown>,
accessPath: string = ''
accessPath: string = "",
) => {
const doc = null;
const value = Object.entries(obj).reduce((acc, [key, val]) => {
// Construct the new access path for nested properties
const newPath = `${accessPath}["${key}"]`;
const value = Object.entries(obj).reduce(
(acc, [key, val]) => {
// Construct the new access path for nested properties
const newPath = `${accessPath}["${key}"]`;
// Serialize each value in the dictionary and assign to the accumulator
if (
typeof val === 'number' ||
typeof val === 'boolean' ||
typeof val === 'string' ||
val === null
) {
acc[key] = serializePrimitive(val as number | boolean | string | null, newPath);
}
// Serialize each value in the dictionary and assign to the accumulator
if (
typeof val === "number" ||
typeof val === "boolean" ||
typeof val === "string" ||
val === null
) {
acc[key] = serializePrimitive(val as number | boolean | string | null, newPath);
}
return acc;
}, {});
return acc;
},
<Record<string, SerializedObject>>{},
);
return {
full_access_path: accessPath,
type: 'dict',
type: "dict",
value,
readonly: false,
doc
doc,
};
};

View File

@ -1,9 +1,9 @@
import { SerializedValue } from '../components/GenericComponent';
import { SerializedObject } from "../types/SerializedObject";
export type State = {
type: string;
name: string;
value: Record<string, SerializedValue> | null;
value: Record<string, SerializedObject> | null;
readonly: boolean;
doc: string | null;
};
@ -45,7 +45,7 @@ export function parseFullAccessPath(path: string): string[] {
*/
function parseSerializedKey(serializedKey: string): string | number {
// Strip outer brackets if present
if (serializedKey.startsWith('[') && serializedKey.endsWith(']')) {
if (serializedKey.startsWith("[") && serializedKey.endsWith("]")) {
serializedKey = serializedKey.slice(1, -1);
}
@ -68,12 +68,13 @@ function parseSerializedKey(serializedKey: string): string | number {
}
function getOrCreateItemInContainer(
container: Record<string | number, SerializedValue> | SerializedValue[],
container: Record<string | number, SerializedObject> | SerializedObject[],
key: string | number,
allowAddKey: boolean
): SerializedValue {
allowAddKey: boolean,
): SerializedObject {
// Check if the key exists and return the item if it does
if (key in container) {
/* @ts-expect-error Key is in the correct form but converted to type any for some reason */
return container[key];
}
@ -107,10 +108,10 @@ function getOrCreateItemInContainer(
* @throws SerializationValueError If the expected structure is incorrect.
*/
function getContainerItemByKey(
container: Record<string, SerializedValue> | SerializedValue[],
container: Record<string, SerializedObject> | SerializedObject[],
key: string,
allowAppend: boolean = false
): SerializedValue {
allowAppend: boolean = false,
): SerializedObject {
const processedKey = parseSerializedKey(key);
try {
@ -126,13 +127,13 @@ function getContainerItemByKey(
}
export function setNestedValueByPath(
serializationDict: Record<string, SerializedValue>,
serializationDict: Record<string, SerializedObject>,
path: string,
serializedValue: SerializedValue
): Record<string, SerializedValue> {
serializedValue: SerializedObject,
): Record<string, SerializedObject> {
const pathParts = parseFullAccessPath(path);
const newSerializationDict: Record<string, SerializedValue> = JSON.parse(
JSON.stringify(serializationDict)
const newSerializationDict: Record<string, SerializedObject> = JSON.parse(
JSON.stringify(serializationDict),
);
let currentDict = newSerializationDict;
@ -143,11 +144,11 @@ export function setNestedValueByPath(
const nextLevelSerializedObject = getContainerItemByKey(
currentDict,
pathPart,
false
false,
);
currentDict = nextLevelSerializedObject['value'] as Record<
currentDict = nextLevelSerializedObject["value"] as Record<
string,
SerializedValue
SerializedObject
>;
}
@ -160,14 +161,15 @@ export function setNestedValueByPath(
} catch (error) {
console.error(`Error occurred trying to change ${path}: ${error}`);
}
return {};
}
function createEmptySerializedObject(): SerializedValue {
function createEmptySerializedObject(): SerializedObject {
return {
full_access_path: '',
value: undefined,
type: 'None',
full_access_path: "",
value: null,
type: "None",
doc: null,
readonly: false
readonly: false,
};
}

View File

@ -1,16 +1,16 @@
export function getIdFromFullAccessPath(fullAccessPath: string) {
if (fullAccessPath) {
// Replace '].' with a single dash
let id = fullAccessPath.replace(/\]\./g, '-');
let id = fullAccessPath.replace(/\]\./g, "-");
// Replace any character that is not a word character or underscore with a dash
id = id.replace(/[^\w_]+/g, '-');
id = id.replace(/[^\w_]+/g, "-");
// Remove any trailing dashes
id = id.replace(/-+$/, '');
id = id.replace(/-+$/, "");
return id;
} else {
return 'main';
return "main";
}
}

View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
}

View File

@ -1,8 +1,11 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"noEmit": true,
"esModuleInterop": true
}
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": [
"vite.config.ts"
]
}

13
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
outDir: "../src/pydase/frontend",
},
esbuild: {
drop: ["console", "debugger"],
},
});