mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-04-21 00:40:01 +02:00
feat: moving from react-create-app to vite
- loads of type fixes - configuration changes
This commit is contained in:
parent
c0734d58ce
commit
73a3283a7d
@ -8,9 +8,12 @@
|
|||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"prettier"
|
"prettier",
|
||||||
|
"plugin:react/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error"
|
"prettier/prettier": "error",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/prop-types": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
frontend/.gitignore
vendored
38
frontend/.gitignore
vendored
@ -1,20 +1,24 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Logs
|
||||||
|
logs
|
||||||
# dependencies
|
*.log
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.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?
|
||||||
|
@ -3,9 +3,7 @@
|
|||||||
"bracketSameLine": true,
|
"bracketSameLine": true,
|
||||||
"endOfLine": "auto",
|
"endOfLine": "auto",
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"vueIndentScriptAndStyle": true,
|
"printWidth": 88
|
||||||
"printWidth": 88,
|
|
||||||
"trailingComma": "none"
|
|
||||||
}
|
}
|
||||||
|
@ -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.\
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
|
||||||
|
|
||||||
The page will reload when you make changes.\
|
- Configure the top-level `parserOptions` property like this:
|
||||||
You may also see any lint errors in the console.
|
|
||||||
|
|
||||||
### `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.\
|
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
- 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
|
||||||
### `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)
|
|
||||||
|
17
frontend/index.html
Normal file
17
frontend/index.html
Normal 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>
|
4146
frontend/package-lock.json
generated
4146
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,58 +1,43 @@
|
|||||||
{
|
{
|
||||||
"name": "pydase",
|
"name": "pydase",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"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": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fsouza/prettierd": "^0.25.1",
|
|
||||||
"@mui/material": "^5.14.1",
|
"@mui/material": "^5.14.1",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"bootstrap": "^5.3.3",
|
||||||
"@testing-library/react": "^13.4.0",
|
"react": "^18.3.1",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"react-bootstrap": "^2.10.0",
|
||||||
"bootstrap": "^5.3.0",
|
"react-bootstrap-icons": "^1.11.4",
|
||||||
"react": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-bootstrap": "^2.8.0",
|
"react-scripts": "^5.0.1",
|
||||||
"react-bootstrap-icons": "^1.10.3",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
"web-vitals": "^3.4.0"
|
"web-vitals": "^4.2.1"
|
||||||
},
|
|
||||||
"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": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.0.0",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
"@typescript-eslint/parser": "^7.13.1",
|
||||||
"@typescript-eslint/parser": "^6.9.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"eslint": "^8.52.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-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^4.9.0"
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.3.1"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
@ -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"
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
@ -1,43 +1,49 @@
|
|||||||
import { useCallback, useEffect, useReducer, useState } from 'react';
|
import { useCallback, useEffect, useReducer, useState } from "react";
|
||||||
import { Navbar, Form, Offcanvas, Container } from 'react-bootstrap';
|
import { Navbar, Form, Offcanvas, Container } from "react-bootstrap";
|
||||||
import { hostname, port, socket } from './socket';
|
import { hostname, port, socket } from "./socket";
|
||||||
import './App.css';
|
import "./App.css";
|
||||||
import {
|
import {
|
||||||
Notifications,
|
Notifications,
|
||||||
Notification,
|
Notification,
|
||||||
LevelName
|
LevelName,
|
||||||
} from './components/NotificationsComponent';
|
} from "./components/NotificationsComponent";
|
||||||
import { ConnectionToast } from './components/ConnectionToast';
|
import { ConnectionToast } from "./components/ConnectionToast";
|
||||||
import { setNestedValueByPath, State } from './utils/stateUtils';
|
import { setNestedValueByPath, State } from "./utils/stateUtils";
|
||||||
import { WebSettingsContext, WebSetting } from './WebSettings';
|
import { WebSettingsContext, WebSetting } from "./WebSettings";
|
||||||
import { SerializedValue, GenericComponent } from './components/GenericComponent';
|
import { GenericComponent } from "./components/GenericComponent";
|
||||||
|
import { SerializedObject } from "./types/SerializedObject";
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_DATA'; data: State }
|
| { type: "SET_DATA"; data: State }
|
||||||
| {
|
| {
|
||||||
type: 'UPDATE_ATTRIBUTE';
|
type: "UPDATE_ATTRIBUTE";
|
||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
newValue: SerializedValue;
|
newValue: SerializedObject;
|
||||||
};
|
};
|
||||||
type UpdateMessage = {
|
type UpdateMessage = {
|
||||||
data: { full_access_path: string; value: SerializedValue };
|
data: { full_access_path: string; value: SerializedObject };
|
||||||
};
|
};
|
||||||
type LogMessage = {
|
type LogMessage = {
|
||||||
levelname: LevelName;
|
levelname: LevelName;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state: State, action: Action): State => {
|
const reducer = (state: State | null, action: Action): State | null => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_DATA':
|
case "SET_DATA":
|
||||||
return action.data;
|
return action.data;
|
||||||
case 'UPDATE_ATTRIBUTE': {
|
case "UPDATE_ATTRIBUTE": {
|
||||||
if (state === null) {
|
if (state === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...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:
|
default:
|
||||||
@ -46,19 +52,19 @@ const reducer = (state: State, action: Action): State => {
|
|||||||
};
|
};
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, null);
|
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 [webSettings, setWebSettings] = useState<Record<string, WebSetting>>({});
|
||||||
const [isInstantUpdate, setIsInstantUpdate] = useState(() => {
|
const [isInstantUpdate, setIsInstantUpdate] = useState(() => {
|
||||||
const saved = localStorage.getItem('isInstantUpdate');
|
const saved = localStorage.getItem("isInstantUpdate");
|
||||||
return saved !== null ? JSON.parse(saved) : false;
|
return saved !== null ? JSON.parse(saved) : false;
|
||||||
});
|
});
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [showNotification, setShowNotification] = useState(() => {
|
const [showNotification, setShowNotification] = useState(() => {
|
||||||
const saved = localStorage.getItem('showNotification');
|
const saved = localStorage.getItem("showNotification");
|
||||||
return saved !== null ? JSON.parse(saved) : false;
|
return saved !== null ? JSON.parse(saved) : false;
|
||||||
});
|
});
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
const [connectionStatus, setConnectionStatus] = useState('connecting');
|
const [connectionStatus, setConnectionStatus] = useState("connecting");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Allow the user to add a custom css file
|
// Allow the user to add a custom css file
|
||||||
@ -66,21 +72,21 @@ const App = () => {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// If the file exists, create a link element for the custom CSS
|
// 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.href = `http://${hostname}:${port}/custom.css`;
|
||||||
link.type = 'text/css';
|
link.type = "text/css";
|
||||||
link.rel = 'stylesheet';
|
link.rel = "stylesheet";
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error); // Handle the error appropriately
|
.catch(console.error); // Handle the error appropriately
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on("connect", () => {
|
||||||
// Fetch data from the API when the client connects
|
// Fetch data from the API when the client connects
|
||||||
fetch(`http://${hostname}:${port}/service-properties`)
|
fetch(`http://${hostname}:${port}/service-properties`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data: State) => {
|
.then((data: State) => {
|
||||||
dispatch({ type: 'SET_DATA', data });
|
dispatch({ type: "SET_DATA", data });
|
||||||
setServiceName(data.name);
|
setServiceName(data.name);
|
||||||
|
|
||||||
document.title = data.name; // Setting browser tab title
|
document.title = data.name; // Setting browser tab title
|
||||||
@ -88,40 +94,40 @@ const App = () => {
|
|||||||
fetch(`http://${hostname}:${port}/web-settings`)
|
fetch(`http://${hostname}:${port}/web-settings`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data: Record<string, WebSetting>) => setWebSettings(data));
|
.then((data: Record<string, WebSetting>) => setWebSettings(data));
|
||||||
setConnectionStatus('connected');
|
setConnectionStatus("connected");
|
||||||
});
|
});
|
||||||
socket.on('disconnect', () => {
|
socket.on("disconnect", () => {
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus("disconnected");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Only set "reconnecting" is the state is still "disconnected"
|
// Only set "reconnecting" is the state is still "disconnected"
|
||||||
// E.g. when the client has already reconnected
|
// E.g. when the client has already reconnected
|
||||||
setConnectionStatus((currentState) =>
|
setConnectionStatus((currentState) =>
|
||||||
currentState === 'disconnected' ? 'reconnecting' : currentState
|
currentState === "disconnected" ? "reconnecting" : currentState,
|
||||||
);
|
);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('notify', onNotify);
|
socket.on("notify", onNotify);
|
||||||
socket.on('log', onLogMessage);
|
socket.on("log", onLogMessage);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('notify', onNotify);
|
socket.off("notify", onNotify);
|
||||||
socket.off('log', onLogMessage);
|
socket.off("log", onLogMessage);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Persist isInstantUpdate and showNotification state changes to localStorage
|
// Persist isInstantUpdate and showNotification state changes to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('isInstantUpdate', JSON.stringify(isInstantUpdate));
|
localStorage.setItem("isInstantUpdate", JSON.stringify(isInstantUpdate));
|
||||||
}, [isInstantUpdate]);
|
}, [isInstantUpdate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('showNotification', JSON.stringify(showNotification));
|
localStorage.setItem("showNotification", JSON.stringify(showNotification));
|
||||||
}, [showNotification]);
|
}, [showNotification]);
|
||||||
// Adding useCallback to prevent notify to change causing a re-render of all
|
// Adding useCallback to prevent notify to change causing a re-render of all
|
||||||
// components
|
// components
|
||||||
const addNotification = useCallback(
|
const addNotification = useCallback(
|
||||||
(message: string, levelname: LevelName = 'DEBUG') => {
|
(message: string, levelname: LevelName = "DEBUG") => {
|
||||||
// Getting the current time in the required format
|
// Getting the current time in the required format
|
||||||
const timeStamp = new Date().toISOString().substring(11, 19);
|
const timeStamp = new Date().toISOString().substring(11, 19);
|
||||||
// Adding an id to the notification to provide a way of removing it
|
// Adding an id to the notification to provide a way of removing it
|
||||||
@ -130,15 +136,15 @@ const App = () => {
|
|||||||
// Custom logic for notifications
|
// Custom logic for notifications
|
||||||
setNotifications((prevNotifications) => [
|
setNotifications((prevNotifications) => [
|
||||||
{ levelname, id, message, timeStamp },
|
{ levelname, id, message, timeStamp },
|
||||||
...prevNotifications
|
...prevNotifications,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeNotificationById = (id: number) => {
|
const removeNotificationById = (id: number) => {
|
||||||
setNotifications((prevNotifications) =>
|
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
|
// Dispatching the update to the reducer
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_ATTRIBUTE',
|
type: "UPDATE_ATTRIBUTE",
|
||||||
fullAccessPath,
|
fullAccessPath,
|
||||||
newValue
|
newValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +214,7 @@ const App = () => {
|
|||||||
<div className="App navbarOffset">
|
<div className="App navbarOffset">
|
||||||
<WebSettingsContext.Provider value={webSettings}>
|
<WebSettingsContext.Provider value={webSettings}>
|
||||||
<GenericComponent
|
<GenericComponent
|
||||||
attribute={state as SerializedValue}
|
attribute={state as SerializedObject}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from "react";
|
||||||
|
|
||||||
export const WebSettingsContext = createContext<Record<string, WebSetting>>({});
|
export const WebSettingsContext = createContext<Record<string, WebSetting>>({});
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { runMethod } from '../socket';
|
import { runMethod } from "../socket";
|
||||||
import { Form, Button, InputGroup, Spinner } from 'react-bootstrap';
|
import { Form, Button, InputGroup, Spinner } from "react-bootstrap";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
|
|
||||||
type AsyncMethodProps = {
|
type AsyncMethodProps = {
|
||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
value: 'RUNNING' | null;
|
value: "RUNNING" | null;
|
||||||
docString?: string;
|
docString: string | null;
|
||||||
hideOutput?: boolean;
|
hideOutput?: boolean;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -22,7 +22,7 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
|||||||
value: runningTask,
|
value: runningTask,
|
||||||
addNotification,
|
addNotification,
|
||||||
displayName,
|
displayName,
|
||||||
id
|
id,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// Conditional rendering based on the 'render' prop.
|
// Conditional rendering based on the 'render' prop.
|
||||||
@ -33,7 +33,7 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
|||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
const [spinning, setSpinning] = useState(false);
|
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));
|
const parentPath = fullAccessPath.slice(0, -(name.length + 1));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -59,14 +59,14 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
|||||||
method_name = `start_${name}`;
|
method_name = `start_${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessPath = [parentPath, method_name].filter((element) => element).join('.');
|
const accessPath = [parentPath, method_name].filter((element) => element).join(".");
|
||||||
setSpinning(true);
|
setSpinning(true);
|
||||||
runMethod(accessPath);
|
runMethod(accessPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="component asyncMethodComponent" id={id}>
|
<div className="component asyncMethodComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<Form onSubmit={execute} ref={formRef}>
|
<Form onSubmit={execute} ref={formRef}>
|
||||||
@ -78,10 +78,10 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
|||||||
<Button id={`button-${id}`} type="submit">
|
<Button id={`button-${id}`} type="submit">
|
||||||
{spinning ? (
|
{spinning ? (
|
||||||
<Spinner size="sm" role="status" aria-hidden="true" />
|
<Spinner size="sm" role="status" aria-hidden="true" />
|
||||||
) : runningTask === 'RUNNING' ? (
|
) : runningTask === "RUNNING" ? (
|
||||||
'Stop '
|
"Stop "
|
||||||
) : (
|
) : (
|
||||||
'Start '
|
"Start "
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
@ -89,3 +89,5 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AsyncMethodComponent.displayName = "AsyncMethodComponent";
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from "react";
|
||||||
import { ToggleButton } from 'react-bootstrap';
|
import { ToggleButton } from "react-bootstrap";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import { SerializedValue } from './GenericComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
|
||||||
type ButtonComponentProps = {
|
type ButtonComponentProps = {
|
||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
value: boolean;
|
value: boolean;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
docString: string;
|
docString: string | null;
|
||||||
mapping?: [string, string]; // Enforce a tuple of two strings
|
mapping?: [string, string]; // Enforce a tuple of two strings
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
|
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@ -25,7 +25,7 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
|
|||||||
addNotification,
|
addNotification,
|
||||||
changeCallback = () => {},
|
changeCallback = () => {},
|
||||||
displayName,
|
displayName,
|
||||||
id
|
id,
|
||||||
} = props;
|
} = props;
|
||||||
// const buttonName = props.mapping ? (value ? props.mapping[0] : props.mapping[1]) : name;
|
// 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) => {
|
const setChecked = (checked: boolean) => {
|
||||||
changeCallback({
|
changeCallback({
|
||||||
type: 'bool',
|
type: "bool",
|
||||||
value: checked,
|
value: checked,
|
||||||
full_access_path: fullAccessPath,
|
full_access_path: fullAccessPath,
|
||||||
readonly: readOnly,
|
readonly: readOnly,
|
||||||
doc: docString
|
doc: docString,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'component buttonComponent'} id={id}>
|
<div className={"component buttonComponent"} id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
id={`toggle-check-${id}`}
|
id={`toggle-check-${id}`}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
variant={value ? 'success' : 'secondary'}
|
variant={value ? "success" : "secondary"}
|
||||||
checked={value}
|
checked={value}
|
||||||
value={displayName}
|
value={displayName}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
@ -69,3 +69,5 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ButtonComponent.displayName = "ButtonComponent";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { Toast, Button, ToastContainer } from 'react-bootstrap';
|
import { Toast, Button, ToastContainer } from "react-bootstrap";
|
||||||
|
|
||||||
type ConnectionToastProps = {
|
type ConnectionToastProps = {
|
||||||
connectionStatus: string;
|
connectionStatus: string;
|
||||||
@ -36,31 +36,31 @@ export const ConnectionToast = React.memo(
|
|||||||
delay: number | undefined;
|
delay: number | undefined;
|
||||||
} => {
|
} => {
|
||||||
switch (connectionStatus) {
|
switch (connectionStatus) {
|
||||||
case 'connecting':
|
case "connecting":
|
||||||
return {
|
return {
|
||||||
message: 'Connecting...',
|
message: "Connecting...",
|
||||||
bg: 'info',
|
bg: "info",
|
||||||
delay: undefined
|
delay: undefined,
|
||||||
};
|
};
|
||||||
case 'connected':
|
case "connected":
|
||||||
return { message: 'Connected', bg: 'success', delay: 1000 };
|
return { message: "Connected", bg: "success", delay: 1000 };
|
||||||
case 'disconnected':
|
case "disconnected":
|
||||||
return {
|
return {
|
||||||
message: 'Disconnected',
|
message: "Disconnected",
|
||||||
bg: 'danger',
|
bg: "danger",
|
||||||
delay: undefined
|
delay: undefined,
|
||||||
};
|
};
|
||||||
case 'reconnecting':
|
case "reconnecting":
|
||||||
return {
|
return {
|
||||||
message: 'Reconnecting...',
|
message: "Reconnecting...",
|
||||||
bg: 'info',
|
bg: "info",
|
||||||
delay: undefined
|
delay: undefined,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
message: '',
|
message: "",
|
||||||
bg: 'info',
|
bg: "info",
|
||||||
delay: undefined
|
delay: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -82,5 +82,7 @@ export const ConnectionToast = React.memo(
|
|||||||
</Toast>
|
</Toast>
|
||||||
</ToastContainer>
|
</ToastContainer>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ConnectionToast.displayName = "ConnectionToast";
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Card, Collapse } from 'react-bootstrap';
|
import { Card, Collapse } from "react-bootstrap";
|
||||||
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
|
import { ChevronDown, ChevronRight } from "react-bootstrap-icons";
|
||||||
import { SerializedValue, GenericComponent } from './GenericComponent';
|
import { GenericComponent } from "./GenericComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
|
||||||
type DataServiceProps = {
|
type DataServiceProps = {
|
||||||
props: DataServiceJSON;
|
props: DataServiceJSON;
|
||||||
@ -13,7 +14,7 @@ type DataServiceProps = {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataServiceJSON = Record<string, SerializedValue>;
|
export type DataServiceJSON = Record<string, SerializedObject>;
|
||||||
|
|
||||||
export const DataServiceComponent = React.memo(
|
export const DataServiceComponent = React.memo(
|
||||||
({ props, isInstantUpdate, addNotification, displayName, id }: DataServiceProps) => {
|
({ props, isInstantUpdate, addNotification, displayName, id }: DataServiceProps) => {
|
||||||
@ -28,11 +29,11 @@ export const DataServiceComponent = React.memo(
|
|||||||
localStorage.setItem(`dataServiceComponent-${id}-open`, JSON.stringify(open));
|
localStorage.setItem(`dataServiceComponent-${id}-open`, JSON.stringify(open));
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
if (displayName !== '') {
|
if (displayName !== "") {
|
||||||
return (
|
return (
|
||||||
<div className="component dataServiceComponent" id={id}>
|
<div className="component dataServiceComponent" id={id}>
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Header onClick={() => setOpen(!open)} style={{ cursor: 'pointer' }}>
|
<Card.Header onClick={() => setOpen(!open)} style={{ cursor: "pointer" }}>
|
||||||
{displayName} {open ? <ChevronDown /> : <ChevronRight />}
|
{displayName} {open ? <ChevronDown /> : <ChevronRight />}
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Collapse in={open}>
|
<Collapse in={open}>
|
||||||
@ -64,5 +65,7 @@ export const DataServiceComponent = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DataServiceComponent.displayName = "DataServiceComponent";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
|
import { DataServiceComponent, DataServiceJSON } from "./DataServiceComponent";
|
||||||
import { MethodComponent } from './MethodComponent';
|
import { MethodComponent } from "./MethodComponent";
|
||||||
|
|
||||||
type DeviceConnectionProps = {
|
type DeviceConnectionProps = {
|
||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
@ -19,7 +19,7 @@ export const DeviceConnectionComponent = React.memo(
|
|||||||
isInstantUpdate,
|
isInstantUpdate,
|
||||||
addNotification,
|
addNotification,
|
||||||
displayName,
|
displayName,
|
||||||
id
|
id,
|
||||||
}: DeviceConnectionProps) => {
|
}: DeviceConnectionProps) => {
|
||||||
const { connected, connect, ...updatedProps } = props;
|
const { connected, connect, ...updatedProps } = props;
|
||||||
const connectedVal = connected.value;
|
const connectedVal = connected.value;
|
||||||
@ -29,14 +29,14 @@ export const DeviceConnectionComponent = React.memo(
|
|||||||
{!connectedVal && (
|
{!connectedVal && (
|
||||||
<div className="overlayContent">
|
<div className="overlayContent">
|
||||||
<div>
|
<div>
|
||||||
{displayName != '' ? displayName : 'Device'} is currently not available!
|
{displayName != "" ? displayName : "Device"} is currently not available!
|
||||||
</div>
|
</div>
|
||||||
<MethodComponent
|
<MethodComponent
|
||||||
fullAccessPath={`${fullAccessPath}.connect`}
|
fullAccessPath={`${fullAccessPath}.connect`}
|
||||||
docString={connect.doc}
|
docString={connect.doc}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
displayName={'reconnect'}
|
displayName={"reconnect"}
|
||||||
id={id + '-connect'}
|
id={id + "-connect"}
|
||||||
render={true}
|
render={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -50,5 +50,7 @@ export const DeviceConnectionComponent = React.memo(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DeviceConnectionComponent.displayName = "DeviceConnectionComponent";
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from "react";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import { SerializedValue, GenericComponent } from './GenericComponent';
|
import { GenericComponent } from "./GenericComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
|
||||||
type DictComponentProps = {
|
type DictComponentProps = {
|
||||||
value: Record<string, SerializedValue>;
|
value: Record<string, SerializedObject>;
|
||||||
docString: string;
|
docString: string | null;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
id: string;
|
id: string;
|
||||||
@ -22,8 +23,8 @@ export const DictComponent = React.memo((props: DictComponentProps) => {
|
|||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'listComponent'} id={id}>
|
<div className={"listComponent"} id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
@ -40,3 +41,5 @@ export const DictComponent = React.memo((props: DictComponentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DictComponent.displayName = "DictComponent";
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Badge, Tooltip, OverlayTrigger } from 'react-bootstrap';
|
import { Badge, Tooltip, OverlayTrigger } from "react-bootstrap";
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
type DocStringProps = {
|
type DocStringProps = {
|
||||||
docString?: string;
|
docString?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocStringComponent = React.memo((props: DocStringProps) => {
|
export const DocStringComponent = React.memo((props: DocStringProps) => {
|
||||||
@ -21,3 +21,5 @@ export const DocStringComponent = React.memo((props: DocStringProps) => {
|
|||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DocStringComponent.displayName = "DocStringComponent";
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { InputGroup, Form, Row, Col } from 'react-bootstrap';
|
import { InputGroup, Form, Row, Col } from "react-bootstrap";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import { SerializedValue } from './GenericComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
|
||||||
export type EnumSerialization = {
|
export type EnumSerialization = {
|
||||||
type: 'Enum' | 'ColouredEnum';
|
type: "Enum" | "ColouredEnum";
|
||||||
full_access_path: string;
|
full_access_path: string;
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
doc?: string | null;
|
doc: string | null;
|
||||||
enum: Record<string, string>;
|
enum: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ type EnumComponentProps = {
|
|||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
id: 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) => {
|
export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
||||||
@ -29,12 +29,12 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
|||||||
value,
|
value,
|
||||||
doc: docString,
|
doc: docString,
|
||||||
enum: enumDict,
|
enum: enumDict,
|
||||||
readonly: readOnly
|
readonly: readOnly,
|
||||||
} = attribute;
|
} = attribute;
|
||||||
|
|
||||||
let { changeCallback } = props;
|
let { changeCallback } = props;
|
||||||
if (changeCallback === undefined) {
|
if (changeCallback === undefined) {
|
||||||
changeCallback = (value: SerializedValue) => {
|
changeCallback = (value: SerializedObject) => {
|
||||||
setEnumValue(() => {
|
setEnumValue(() => {
|
||||||
return String(value.value);
|
return String(value.value);
|
||||||
});
|
});
|
||||||
@ -55,8 +55,8 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
|||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'component enumComponent'} id={id}>
|
<div className={"component enumComponent"} id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<Row>
|
<Row>
|
||||||
@ -70,11 +70,11 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
|||||||
// Display the Form.Control when readOnly is true
|
// Display the Form.Control when readOnly is true
|
||||||
<Form.Control
|
<Form.Control
|
||||||
style={
|
style={
|
||||||
attribute.type == 'ColouredEnum'
|
attribute.type == "ColouredEnum"
|
||||||
? { backgroundColor: enumDict[enumValue] }
|
? { backgroundColor: enumDict[enumValue] }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
value={attribute.type == 'ColouredEnum' ? enumValue : enumDict[enumValue]}
|
value={attribute.type == "ColouredEnum" ? enumValue : enumDict[enumValue]}
|
||||||
name={fullAccessPath}
|
name={fullAccessPath}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
@ -85,7 +85,7 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
|||||||
value={enumValue}
|
value={enumValue}
|
||||||
name={fullAccessPath}
|
name={fullAccessPath}
|
||||||
style={
|
style={
|
||||||
attribute.type == 'ColouredEnum'
|
attribute.type == "ColouredEnum"
|
||||||
? { backgroundColor: enumDict[enumValue] }
|
? { backgroundColor: enumDict[enumValue] }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
@ -97,12 +97,12 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
|||||||
value: event.target.value,
|
value: event.target.value,
|
||||||
full_access_path: fullAccessPath,
|
full_access_path: fullAccessPath,
|
||||||
readonly: attribute.readonly,
|
readonly: attribute.readonly,
|
||||||
doc: attribute.doc
|
doc: attribute.doc,
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
{Object.entries(enumDict).map(([key, val]) => (
|
{Object.entries(enumDict).map(([key, val]) => (
|
||||||
<option key={key} value={key}>
|
<option key={key} value={key}>
|
||||||
{attribute.type == 'ColouredEnum' ? key : val}
|
{attribute.type == "ColouredEnum" ? key : val}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
@ -112,3 +112,5 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
EnumComponent.displayName = "EnumComponent";
|
||||||
|
@ -1,62 +1,34 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from "react";
|
||||||
import { ButtonComponent } from './ButtonComponent';
|
import { ButtonComponent } from "./ButtonComponent";
|
||||||
import { NumberComponent } from './NumberComponent';
|
import { NumberComponent, NumberObject } from "./NumberComponent";
|
||||||
import { SliderComponent } from './SliderComponent';
|
import { SliderComponent } from "./SliderComponent";
|
||||||
import { EnumComponent, EnumSerialization } from './EnumComponent';
|
import { EnumComponent, EnumSerialization } from "./EnumComponent";
|
||||||
import { MethodComponent } from './MethodComponent';
|
import { MethodComponent } from "./MethodComponent";
|
||||||
import { AsyncMethodComponent } from './AsyncMethodComponent';
|
import { AsyncMethodComponent } from "./AsyncMethodComponent";
|
||||||
import { StringComponent } from './StringComponent';
|
import { StringComponent } from "./StringComponent";
|
||||||
import { ListComponent } from './ListComponent';
|
import { ListComponent } from "./ListComponent";
|
||||||
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
|
import { DataServiceComponent, DataServiceJSON } from "./DataServiceComponent";
|
||||||
import { DeviceConnectionComponent } from './DeviceConnection';
|
import { DeviceConnectionComponent } from "./DeviceConnection";
|
||||||
import { ImageComponent } from './ImageComponent';
|
import { ImageComponent } from "./ImageComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { getIdFromFullAccessPath } from '../utils/stringUtils';
|
import { getIdFromFullAccessPath } from "../utils/stringUtils";
|
||||||
import { WebSettingsContext } from '../WebSettings';
|
import { WebSettingsContext } from "../WebSettings";
|
||||||
import { updateValue } from '../socket';
|
import { updateValue } from "../socket";
|
||||||
import { DictComponent } from './DictComponent';
|
import { DictComponent } from "./DictComponent";
|
||||||
import { parseFullAccessPath } from '../utils/stateUtils';
|
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 = {
|
type GenericComponentProps = {
|
||||||
attribute: SerializedValue;
|
attribute: SerializedObject;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPathFromPathParts = (pathParts: string[]): string => {
|
const getPathFromPathParts = (pathParts: string[]): string => {
|
||||||
let path = '';
|
let path = "";
|
||||||
for (const pathPart of pathParts) {
|
for (const pathPart of pathParts) {
|
||||||
if (!pathPart.startsWith('[') && path !== '') {
|
if (!pathPart.startsWith("[") && path !== "") {
|
||||||
path += '.';
|
path += ".";
|
||||||
}
|
}
|
||||||
path += pathPart;
|
path += pathPart;
|
||||||
}
|
}
|
||||||
@ -69,7 +41,7 @@ const createDisplayNameFromAccessPath = (fullAccessPath: string): string => {
|
|||||||
for (let i = parsedFullAccessPath.length - 1; i >= 0; i--) {
|
for (let i = parsedFullAccessPath.length - 1; i >= 0; i--) {
|
||||||
const item = parsedFullAccessPath[i];
|
const item = parsedFullAccessPath[i];
|
||||||
displayNameParts.unshift(item);
|
displayNameParts.unshift(item);
|
||||||
if (!item.startsWith('[')) {
|
if (!item.startsWith("[")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,13 +66,13 @@ export const GenericComponent = React.memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeCallback(
|
function changeCallback(
|
||||||
value: SerializedValue,
|
value: SerializedObject,
|
||||||
callback: (ack: unknown) => void = undefined
|
callback: (ack: unknown) => void = () => {},
|
||||||
) {
|
) {
|
||||||
updateValue(value, callback);
|
updateValue(value, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attribute.type === 'bool') {
|
if (attribute.type === "bool") {
|
||||||
return (
|
return (
|
||||||
<ButtonComponent
|
<ButtonComponent
|
||||||
fullAccessPath={fullAccessPath}
|
fullAccessPath={fullAccessPath}
|
||||||
@ -113,7 +85,7 @@ export const GenericComponent = React.memo(
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'float' || attribute.type === 'int') {
|
} else if (attribute.type === "float" || attribute.type === "int") {
|
||||||
return (
|
return (
|
||||||
<NumberComponent
|
<NumberComponent
|
||||||
type={attribute.type}
|
type={attribute.type}
|
||||||
@ -128,15 +100,15 @@ export const GenericComponent = React.memo(
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'Quantity') {
|
} else if (attribute.type === "Quantity") {
|
||||||
return (
|
return (
|
||||||
<NumberComponent
|
<NumberComponent
|
||||||
type="Quantity"
|
type="Quantity"
|
||||||
fullAccessPath={fullAccessPath}
|
fullAccessPath={fullAccessPath}
|
||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
readOnly={attribute.readonly}
|
readOnly={attribute.readonly}
|
||||||
value={Number(attribute.value['magnitude'])}
|
value={Number(attribute.value["magnitude"])}
|
||||||
unit={attribute.value['unit']}
|
unit={attribute.value["unit"]}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
changeCallback={changeCallback}
|
changeCallback={changeCallback}
|
||||||
@ -144,16 +116,16 @@ export const GenericComponent = React.memo(
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'NumberSlider') {
|
} else if (attribute.type === "NumberSlider") {
|
||||||
return (
|
return (
|
||||||
<SliderComponent
|
<SliderComponent
|
||||||
fullAccessPath={fullAccessPath}
|
fullAccessPath={fullAccessPath}
|
||||||
docString={attribute.value['value'].doc}
|
docString={attribute.value["value"].doc}
|
||||||
readOnly={attribute.readonly}
|
readOnly={attribute.readonly}
|
||||||
value={attribute.value['value']}
|
value={attribute.value["value"] as NumberObject}
|
||||||
min={attribute.value['min']}
|
min={attribute.value["min"] as NumberObject}
|
||||||
max={attribute.value['max']}
|
max={attribute.value["max"] as NumberObject}
|
||||||
stepSize={attribute.value['step_size']}
|
stepSize={attribute.value["step_size"] as NumberObject}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
changeCallback={changeCallback}
|
changeCallback={changeCallback}
|
||||||
@ -161,7 +133,7 @@ export const GenericComponent = React.memo(
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'Enum' || attribute.type === 'ColouredEnum') {
|
} else if (attribute.type === "Enum" || attribute.type === "ColouredEnum") {
|
||||||
return (
|
return (
|
||||||
<EnumComponent
|
<EnumComponent
|
||||||
attribute={attribute as EnumSerialization}
|
attribute={attribute as EnumSerialization}
|
||||||
@ -171,7 +143,7 @@ export const GenericComponent = React.memo(
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'method') {
|
} else if (attribute.type === "method") {
|
||||||
if (!attribute.async) {
|
if (!attribute.async) {
|
||||||
return (
|
return (
|
||||||
<MethodComponent
|
<MethodComponent
|
||||||
@ -188,7 +160,7 @@ export const GenericComponent = React.memo(
|
|||||||
<AsyncMethodComponent
|
<AsyncMethodComponent
|
||||||
fullAccessPath={fullAccessPath}
|
fullAccessPath={fullAccessPath}
|
||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
value={attribute.value as 'RUNNING' | null}
|
value={attribute.value as "RUNNING" | null}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
id={id}
|
id={id}
|
||||||
@ -196,7 +168,7 @@ export const GenericComponent = React.memo(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (attribute.type === 'str') {
|
} else if (attribute.type === "str") {
|
||||||
return (
|
return (
|
||||||
<StringComponent
|
<StringComponent
|
||||||
fullAccessPath={fullAccessPath}
|
fullAccessPath={fullAccessPath}
|
||||||
@ -210,7 +182,7 @@ export const GenericComponent = React.memo(
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'DataService') {
|
} else if (attribute.type === "DataService") {
|
||||||
return (
|
return (
|
||||||
<DataServiceComponent
|
<DataServiceComponent
|
||||||
props={attribute.value as DataServiceJSON}
|
props={attribute.value as DataServiceJSON}
|
||||||
@ -220,7 +192,7 @@ export const GenericComponent = React.memo(
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'DeviceConnection') {
|
} else if (attribute.type === "DeviceConnection") {
|
||||||
return (
|
return (
|
||||||
<DeviceConnectionComponent
|
<DeviceConnectionComponent
|
||||||
fullAccessPath={fullAccessPath}
|
fullAccessPath={fullAccessPath}
|
||||||
@ -231,41 +203,42 @@ export const GenericComponent = React.memo(
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'list') {
|
} else if (attribute.type === "list") {
|
||||||
return (
|
return (
|
||||||
<ListComponent
|
<ListComponent
|
||||||
value={attribute.value as SerializedValue[]}
|
value={attribute.value}
|
||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'dict') {
|
} else if (attribute.type === "dict") {
|
||||||
return (
|
return (
|
||||||
<DictComponent
|
<DictComponent
|
||||||
value={attribute.value as Record<string, SerializedValue>}
|
value={attribute.value}
|
||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'Image') {
|
} else if (attribute.type === "Image") {
|
||||||
return (
|
return (
|
||||||
<ImageComponent
|
<ImageComponent
|
||||||
fullAccessPath={fullAccessPath}
|
fullAccessPath={fullAccessPath}
|
||||||
docString={attribute.value['value'].doc}
|
docString={attribute.value["value"].doc}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
id={id}
|
id={id}
|
||||||
addNotification={addNotification}
|
addNotification={addNotification}
|
||||||
// Add any other specific props for the ImageComponent here
|
value={attribute.value["value"]["value"] as string}
|
||||||
value={attribute.value['value']['value'] as string}
|
format={attribute.value["format"]["value"] as string}
|
||||||
format={attribute.value['format']['value'] as string}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <div key={fullAccessPath}>{fullAccessPath}</div>;
|
return <div key={fullAccessPath}>{fullAccessPath}</div>;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
GenericComponent.displayName = "GenericComponent";
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Card, Collapse, Image } from 'react-bootstrap';
|
import { Card, Collapse, Image } from "react-bootstrap";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
|
import { ChevronDown, ChevronRight } from "react-bootstrap-icons";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
|
|
||||||
type ImageComponentProps = {
|
type ImageComponentProps = {
|
||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
value: string;
|
value: string;
|
||||||
docString: string;
|
docString: string | null;
|
||||||
format: string;
|
format: string;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -34,7 +34,7 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
|
|||||||
<Card>
|
<Card>
|
||||||
<Card.Header
|
<Card.Header
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
style={{ cursor: 'pointer' }} // Change cursor style on hover
|
style={{ cursor: "pointer" }} // Change cursor style on hover
|
||||||
>
|
>
|
||||||
{displayName}
|
{displayName}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
@ -42,10 +42,10 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
|
|||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Collapse in={open}>
|
<Collapse in={open}>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<p>Render count: {renderCount.current}</p>
|
||||||
)}
|
)}
|
||||||
{format === '' && value === '' ? (
|
{format === "" && value === "" ? (
|
||||||
<p>No image set in the backend.</p>
|
<p>No image set in the backend.</p>
|
||||||
) : (
|
) : (
|
||||||
<Image src={`data:image/${format.toLowerCase()};base64,${value}`}></Image>
|
<Image src={`data:image/${format.toLowerCase()};base64,${value}`}></Image>
|
||||||
@ -56,3 +56,5 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ImageComponent.displayName = "ImageComponent";
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from "react";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import { SerializedValue, GenericComponent } from './GenericComponent';
|
import { GenericComponent } from "./GenericComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
|
||||||
type ListComponentProps = {
|
type ListComponentProps = {
|
||||||
value: SerializedValue[];
|
value: SerializedObject[];
|
||||||
docString: string;
|
docString: string | null;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
id: string;
|
id: string;
|
||||||
@ -21,8 +22,8 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
|
|||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'listComponent'} id={id}>
|
<div className={"listComponent"} id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
@ -39,3 +40,5 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ListComponent.displayName = "ListComponent";
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from "react";
|
||||||
import { runMethod } from '../socket';
|
import { runMethod } from "../socket";
|
||||||
import { Button, Form } from 'react-bootstrap';
|
import { Button, Form } from "react-bootstrap";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
|
|
||||||
type MethodProps = {
|
type MethodProps = {
|
||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
docString?: string;
|
docString: string | null;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
id: string;
|
id: string;
|
||||||
@ -43,7 +43,7 @@ export const MethodComponent = React.memo((props: MethodProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="component methodComponent" id={id}>
|
<div className="component methodComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<Form onSubmit={execute} ref={formRef}>
|
<Form onSubmit={execute} ref={formRef}>
|
||||||
@ -55,3 +55,5 @@ export const MethodComponent = React.memo((props: MethodProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
MethodComponent.displayName = "MethodComponent";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { ToastContainer, Toast } from 'react-bootstrap';
|
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 = {
|
export type Notification = {
|
||||||
id: number;
|
id: number;
|
||||||
timeStamp: string;
|
timeStamp: string;
|
||||||
@ -23,10 +23,10 @@ export const Notifications = React.memo((props: NotificationProps) => {
|
|||||||
{notifications.map((notification) => {
|
{notifications.map((notification) => {
|
||||||
// Determine if the toast should be shown
|
// Determine if the toast should be shown
|
||||||
const shouldShow =
|
const shouldShow =
|
||||||
notification.levelname === 'ERROR' ||
|
notification.levelname === "ERROR" ||
|
||||||
notification.levelname === 'CRITICAL' ||
|
notification.levelname === "CRITICAL" ||
|
||||||
(showNotification &&
|
(showNotification &&
|
||||||
['WARNING', 'INFO', 'DEBUG'].includes(notification.levelname));
|
["WARNING", "INFO", "DEBUG"].includes(notification.levelname));
|
||||||
|
|
||||||
if (!shouldShow) {
|
if (!shouldShow) {
|
||||||
return null;
|
return null;
|
||||||
@ -34,31 +34,31 @@ export const Notifications = React.memo((props: NotificationProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Toast
|
<Toast
|
||||||
className={notification.levelname.toLowerCase() + 'Toast'}
|
className={notification.levelname.toLowerCase() + "Toast"}
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
onClose={() => removeNotificationById(notification.id)}
|
onClose={() => removeNotificationById(notification.id)}
|
||||||
onClick={() => removeNotificationById(notification.id)}
|
onClick={() => removeNotificationById(notification.id)}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
if (notification.levelname !== 'ERROR') {
|
if (notification.levelname !== "ERROR") {
|
||||||
removeNotificationById(notification.id);
|
removeNotificationById(notification.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
show={true}
|
show={true}
|
||||||
autohide={
|
autohide={
|
||||||
notification.levelname === 'WARNING' ||
|
notification.levelname === "WARNING" ||
|
||||||
notification.levelname === 'INFO' ||
|
notification.levelname === "INFO" ||
|
||||||
notification.levelname === 'DEBUG'
|
notification.levelname === "DEBUG"
|
||||||
}
|
}
|
||||||
delay={
|
delay={
|
||||||
notification.levelname === 'WARNING' ||
|
notification.levelname === "WARNING" ||
|
||||||
notification.levelname === 'INFO' ||
|
notification.levelname === "INFO" ||
|
||||||
notification.levelname === 'DEBUG'
|
notification.levelname === "DEBUG"
|
||||||
? 2000
|
? 2000
|
||||||
: undefined
|
: undefined
|
||||||
}>
|
}>
|
||||||
<Toast.Header
|
<Toast.Header
|
||||||
closeButton={false}
|
closeButton={false}
|
||||||
className={notification.levelname.toLowerCase() + 'Toast text-right'}>
|
className={notification.levelname.toLowerCase() + "Toast text-right"}>
|
||||||
<strong className="me-auto">{notification.levelname}</strong>
|
<strong className="me-auto">{notification.levelname}</strong>
|
||||||
<small>{notification.timeStamp}</small>
|
<small>{notification.timeStamp}</small>
|
||||||
</Toast.Header>
|
</Toast.Header>
|
||||||
@ -69,3 +69,5 @@ export const Notifications = React.memo((props: NotificationProps) => {
|
|||||||
</ToastContainer>
|
</ToastContainer>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Notifications.displayName = "Notifications";
|
||||||
|
@ -1,45 +1,43 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
import { Form, InputGroup } from 'react-bootstrap';
|
import { Form, InputGroup } from "react-bootstrap";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import '../App.css';
|
import "../App.css";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { SerializedValue } from './GenericComponent';
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
import { QuantityMap } from "../types/QuantityMap";
|
||||||
|
|
||||||
// TODO: add button functionality
|
// TODO: add button functionality
|
||||||
|
|
||||||
export type QuantityObject = {
|
export type QuantityObject = {
|
||||||
type: 'Quantity';
|
type: "Quantity";
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
value: {
|
value: QuantityMap;
|
||||||
magnitude: number;
|
doc: string | null;
|
||||||
unit: string;
|
|
||||||
};
|
|
||||||
doc?: string;
|
|
||||||
};
|
};
|
||||||
export type IntObject = {
|
export type IntObject = {
|
||||||
type: 'int';
|
type: "int";
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
doc?: string;
|
doc: string | null;
|
||||||
};
|
};
|
||||||
export type FloatObject = {
|
export type FloatObject = {
|
||||||
type: 'float';
|
type: "float";
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
doc?: string;
|
doc: string | null;
|
||||||
};
|
};
|
||||||
export type NumberObject = IntObject | FloatObject | QuantityObject;
|
export type NumberObject = IntObject | FloatObject | QuantityObject;
|
||||||
|
|
||||||
type NumberComponentProps = {
|
type NumberComponentProps = {
|
||||||
type: 'float' | 'int' | 'Quantity';
|
type: "float" | "int" | "Quantity";
|
||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
value: number;
|
value: number;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
docString: string;
|
docString: string | null;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
|
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@ -49,11 +47,11 @@ type NumberComponentProps = {
|
|||||||
const handleArrowKey = (
|
const handleArrowKey = (
|
||||||
key: string,
|
key: string,
|
||||||
value: string,
|
value: string,
|
||||||
selectionStart: number
|
selectionStart: number,
|
||||||
// selectionEnd: number
|
// selectionEnd: number
|
||||||
) => {
|
) => {
|
||||||
// Split the input value into the integer part and decimal part
|
// 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 beforeDecimalCount = parts[0].length; // Count digits before the decimal
|
||||||
const afterDecimalCount = parts[1] ? parts[1].length : 0; // Count digits after 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
|
// Convert the input value to a number, increment or decrement it based on the
|
||||||
// arrow key
|
// 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
|
// Convert the resulting number to a string, maintaining the same number of digits
|
||||||
// after the decimal
|
// after the decimal
|
||||||
const newValue = numValue.toFixed(afterDecimalCount);
|
const newValue = numValue.toFixed(afterDecimalCount);
|
||||||
|
|
||||||
// Check if the length of the integer part of the number string has in-/decreased
|
// 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) {
|
if (newBeforeDecimalCount > beforeDecimalCount) {
|
||||||
// Move the cursor one position to the right
|
// Move the cursor one position to the right
|
||||||
selectionStart += 1;
|
selectionStart += 1;
|
||||||
@ -90,18 +88,18 @@ const handleArrowKey = (
|
|||||||
const handleBackspaceKey = (
|
const handleBackspaceKey = (
|
||||||
value: string,
|
value: string,
|
||||||
selectionStart: number,
|
selectionStart: number,
|
||||||
selectionEnd: number
|
selectionEnd: number,
|
||||||
) => {
|
) => {
|
||||||
if (selectionEnd > selectionStart) {
|
if (selectionEnd > selectionStart) {
|
||||||
// If there is a selection, delete all characters in the selection
|
// If there is a selection, delete all characters in the selection
|
||||||
return {
|
return {
|
||||||
value: value.slice(0, selectionStart) + value.slice(selectionEnd),
|
value: value.slice(0, selectionStart) + value.slice(selectionEnd),
|
||||||
selectionStart
|
selectionStart,
|
||||||
};
|
};
|
||||||
} else if (selectionStart > 0) {
|
} else if (selectionStart > 0) {
|
||||||
return {
|
return {
|
||||||
value: value.slice(0, selectionStart - 1) + value.slice(selectionStart),
|
value: value.slice(0, selectionStart - 1) + value.slice(selectionStart),
|
||||||
selectionStart: selectionStart - 1
|
selectionStart: selectionStart - 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { value, selectionStart };
|
return { value, selectionStart };
|
||||||
@ -110,18 +108,18 @@ const handleBackspaceKey = (
|
|||||||
const handleDeleteKey = (
|
const handleDeleteKey = (
|
||||||
value: string,
|
value: string,
|
||||||
selectionStart: number,
|
selectionStart: number,
|
||||||
selectionEnd: number
|
selectionEnd: number,
|
||||||
) => {
|
) => {
|
||||||
if (selectionEnd > selectionStart) {
|
if (selectionEnd > selectionStart) {
|
||||||
// If there is a selection, delete all characters in the selection
|
// If there is a selection, delete all characters in the selection
|
||||||
return {
|
return {
|
||||||
value: value.slice(0, selectionStart) + value.slice(selectionEnd),
|
value: value.slice(0, selectionStart) + value.slice(selectionEnd),
|
||||||
selectionStart
|
selectionStart,
|
||||||
};
|
};
|
||||||
} else if (selectionStart < value.length) {
|
} else if (selectionStart < value.length) {
|
||||||
return {
|
return {
|
||||||
value: value.slice(0, selectionStart) + value.slice(selectionStart + 1),
|
value: value.slice(0, selectionStart) + value.slice(selectionStart + 1),
|
||||||
selectionStart
|
selectionStart,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { value, selectionStart };
|
return { value, selectionStart };
|
||||||
@ -131,12 +129,12 @@ const handleNumericKey = (
|
|||||||
key: string,
|
key: string,
|
||||||
value: string,
|
value: string,
|
||||||
selectionStart: number,
|
selectionStart: number,
|
||||||
selectionEnd: number
|
selectionEnd: number,
|
||||||
) => {
|
) => {
|
||||||
// Check if a number key or a decimal point key is pressed
|
// 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.
|
// Check if value already contains a decimal. If so, ignore input.
|
||||||
console.warn('Invalid input! Ignoring...');
|
console.warn("Invalid input! Ignoring...");
|
||||||
return { value, selectionStart };
|
return { value, selectionStart };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,98 +164,111 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
addNotification,
|
addNotification,
|
||||||
changeCallback = () => {},
|
changeCallback = () => {},
|
||||||
displayName,
|
displayName,
|
||||||
id
|
id,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// Create a state for the cursor position
|
// 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
|
// Create a state for the input string
|
||||||
const [inputString, setInputString] = useState(value.toString());
|
const [inputString, setInputString] = useState(value.toString());
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
const { key, target } = event;
|
const { key, target } = event;
|
||||||
|
console.log(typeof key);
|
||||||
|
|
||||||
|
// Typecast
|
||||||
|
const inputTarget = target as HTMLInputElement;
|
||||||
if (
|
if (
|
||||||
key === 'F1' ||
|
key === "F1" ||
|
||||||
key === 'F5' ||
|
key === "F5" ||
|
||||||
key === 'F12' ||
|
key === "F12" ||
|
||||||
key === 'Tab' ||
|
key === "Tab" ||
|
||||||
key === 'ArrowRight' ||
|
key === "ArrowRight" ||
|
||||||
key === 'ArrowLeft'
|
key === "ArrowLeft"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Get the current input value and cursor position
|
// Get the current input value and cursor position
|
||||||
const { value } = target;
|
const { value } = inputTarget;
|
||||||
let { selectionStart } = target;
|
const selectionEnd = inputTarget.selectionEnd ?? 0;
|
||||||
const { selectionEnd } = target;
|
let selectionStart = inputTarget.selectionStart ?? 0;
|
||||||
|
|
||||||
let newValue: string = value;
|
let newValue: string = value;
|
||||||
if (event.ctrlKey && key === 'a') {
|
if (event.ctrlKey && key === "a") {
|
||||||
// Select everything when pressing Ctrl + a
|
// Select everything when pressing Ctrl + a
|
||||||
target.setSelectionRange(0, target.value.length);
|
inputTarget.setSelectionRange(0, value.length);
|
||||||
return;
|
return;
|
||||||
} else if (key === '-') {
|
} else if (key === "-") {
|
||||||
if (selectionStart === 0 && !value.startsWith('-')) {
|
if (selectionStart === 0 && !value.startsWith("-")) {
|
||||||
newValue = '-' + value;
|
newValue = "-" + value;
|
||||||
selectionStart++;
|
selectionStart++;
|
||||||
} else if (value.startsWith('-') && selectionStart === 1) {
|
} else if (value.startsWith("-") && selectionStart === 1) {
|
||||||
newValue = value.substring(1); // remove minus sign
|
newValue = value.substring(1); // remove minus sign
|
||||||
selectionStart--;
|
selectionStart--;
|
||||||
} else {
|
} else {
|
||||||
return; // Ignore "-" pressed in other positions
|
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
|
// Check if a number key or a decimal point key is pressed
|
||||||
({ value: newValue, selectionStart } = handleNumericKey(
|
({ value: newValue, selectionStart } = handleNumericKey(
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
selectionStart,
|
selectionStart,
|
||||||
selectionEnd
|
selectionEnd,
|
||||||
));
|
));
|
||||||
} else if (key === '.' && (type === 'float' || type === 'Quantity')) {
|
} else if (key === "." && (type === "float" || type === "Quantity")) {
|
||||||
({ value: newValue, selectionStart } = handleNumericKey(
|
({ value: newValue, selectionStart } = handleNumericKey(
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
selectionStart,
|
selectionStart,
|
||||||
selectionEnd
|
selectionEnd,
|
||||||
));
|
));
|
||||||
} else if (key === 'ArrowUp' || key === 'ArrowDown') {
|
} else if (key === "ArrowUp" || key === "ArrowDown") {
|
||||||
({ value: newValue, selectionStart } = handleArrowKey(
|
({ value: newValue, selectionStart } = handleArrowKey(
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
selectionStart
|
selectionStart,
|
||||||
// selectionEnd
|
// selectionEnd
|
||||||
));
|
));
|
||||||
} else if (key === 'Backspace') {
|
} else if (key === "Backspace") {
|
||||||
({ value: newValue, selectionStart } = handleBackspaceKey(
|
({ value: newValue, selectionStart } = handleBackspaceKey(
|
||||||
value,
|
value,
|
||||||
selectionStart,
|
selectionStart,
|
||||||
selectionEnd
|
selectionEnd,
|
||||||
));
|
));
|
||||||
} else if (key === 'Delete') {
|
} else if (key === "Delete") {
|
||||||
({ value: newValue, selectionStart } = handleDeleteKey(
|
({ value: newValue, selectionStart } = handleDeleteKey(
|
||||||
value,
|
value,
|
||||||
selectionStart,
|
selectionStart,
|
||||||
selectionEnd
|
selectionEnd,
|
||||||
));
|
));
|
||||||
} else if (key === 'Enter' && !isInstantUpdate) {
|
} else if (key === "Enter" && !isInstantUpdate) {
|
||||||
let updatedValue: number | Record<string, unknown> = Number(newValue);
|
let serializedObject: SerializedObject;
|
||||||
if (type === 'Quantity') {
|
if (type === "Quantity") {
|
||||||
updatedValue = {
|
serializedObject = {
|
||||||
|
type: "Quantity",
|
||||||
|
value: {
|
||||||
magnitude: Number(newValue),
|
magnitude: Number(newValue),
|
||||||
unit: unit
|
unit: unit,
|
||||||
};
|
} as QuantityMap,
|
||||||
}
|
|
||||||
changeCallback({
|
|
||||||
type: type,
|
|
||||||
value: updatedValue,
|
|
||||||
full_access_path: fullAccessPath,
|
full_access_path: fullAccessPath,
|
||||||
readonly: readOnly,
|
readonly: readOnly,
|
||||||
doc: docString
|
doc: docString,
|
||||||
});
|
};
|
||||||
|
} else {
|
||||||
|
serializedObject = {
|
||||||
|
type: type,
|
||||||
|
value: Number(newValue),
|
||||||
|
full_access_path: fullAccessPath,
|
||||||
|
readonly: readOnly,
|
||||||
|
doc: docString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCallback(serializedObject);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.debug(key);
|
console.debug(key);
|
||||||
@ -266,20 +277,29 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
|
|
||||||
// Update the input value and maintain the cursor position
|
// Update the input value and maintain the cursor position
|
||||||
if (isInstantUpdate) {
|
if (isInstantUpdate) {
|
||||||
let updatedValue: number | Record<string, unknown> = Number(newValue);
|
let serializedObject: SerializedObject;
|
||||||
if (type === 'Quantity') {
|
if (type === "Quantity") {
|
||||||
updatedValue = {
|
serializedObject = {
|
||||||
|
type: "Quantity",
|
||||||
|
value: {
|
||||||
magnitude: Number(newValue),
|
magnitude: Number(newValue),
|
||||||
unit: unit
|
unit: unit,
|
||||||
};
|
} as QuantityMap,
|
||||||
}
|
|
||||||
changeCallback({
|
|
||||||
type: type,
|
|
||||||
value: updatedValue,
|
|
||||||
full_access_path: fullAccessPath,
|
full_access_path: fullAccessPath,
|
||||||
readonly: readOnly,
|
readonly: readOnly,
|
||||||
doc: docString
|
doc: docString,
|
||||||
});
|
};
|
||||||
|
} else {
|
||||||
|
serializedObject = {
|
||||||
|
type: type,
|
||||||
|
value: Number(newValue),
|
||||||
|
full_access_path: fullAccessPath,
|
||||||
|
readonly: readOnly,
|
||||||
|
doc: docString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCallback(serializedObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputString(newValue);
|
setInputString(newValue);
|
||||||
@ -291,26 +311,35 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
if (!isInstantUpdate) {
|
if (!isInstantUpdate) {
|
||||||
// If not in "instant update" mode, emit an update when the input field loses focus
|
// If not in "instant update" mode, emit an update when the input field loses focus
|
||||||
let updatedValue: number | Record<string, unknown> = Number(inputString);
|
let serializedObject: SerializedObject;
|
||||||
if (type === 'Quantity') {
|
if (type === "Quantity") {
|
||||||
updatedValue = {
|
serializedObject = {
|
||||||
|
type: "Quantity",
|
||||||
|
value: {
|
||||||
magnitude: Number(inputString),
|
magnitude: Number(inputString),
|
||||||
unit: unit
|
unit: unit,
|
||||||
};
|
} as QuantityMap,
|
||||||
}
|
|
||||||
changeCallback({
|
|
||||||
type: type,
|
|
||||||
value: updatedValue,
|
|
||||||
full_access_path: fullAccessPath,
|
full_access_path: fullAccessPath,
|
||||||
readonly: readOnly,
|
readonly: readOnly,
|
||||||
doc: docString
|
doc: docString,
|
||||||
});
|
};
|
||||||
|
} else {
|
||||||
|
serializedObject = {
|
||||||
|
type: type,
|
||||||
|
value: Number(inputString),
|
||||||
|
full_access_path: fullAccessPath,
|
||||||
|
readonly: readOnly,
|
||||||
|
doc: docString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCallback(serializedObject);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Parse the input string to a number for comparison
|
// Parse the input string to a number for comparison
|
||||||
const numericInputString =
|
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
|
// Only update the inputString if it's different from the prop value
|
||||||
if (value !== numericInputString) {
|
if (value !== numericInputString) {
|
||||||
setInputString(value.toString());
|
setInputString(value.toString());
|
||||||
@ -319,7 +348,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
// emitting notification
|
// emitting notification
|
||||||
let notificationMsg = `${fullAccessPath} changed to ${props.value}`;
|
let notificationMsg = `${fullAccessPath} changed to ${props.value}`;
|
||||||
if (unit === undefined) {
|
if (unit === undefined) {
|
||||||
notificationMsg += '.';
|
notificationMsg += ".";
|
||||||
} else {
|
} else {
|
||||||
notificationMsg += ` ${unit}.`;
|
notificationMsg += ` ${unit}.`;
|
||||||
}
|
}
|
||||||
@ -336,7 +365,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="component numberComponent" id={id}>
|
<div className="component numberComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
@ -354,10 +383,12 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
name={id}
|
name={id}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
|
className={isInstantUpdate && !readOnly ? "instantUpdate" : ""}
|
||||||
/>
|
/>
|
||||||
{unit && <InputGroup.Text>{unit}</InputGroup.Text>}
|
{unit && <InputGroup.Text>{unit}</InputGroup.Text>}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
NumberComponent.displayName = "NumberComponent";
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { InputGroup, Form, Row, Col, Collapse, ToggleButton } from 'react-bootstrap';
|
import { InputGroup, Form, Row, Col, Collapse, ToggleButton } from "react-bootstrap";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import { Slider } from '@mui/material';
|
import { Slider } from "@mui/material";
|
||||||
import { NumberComponent, NumberObject } from './NumberComponent';
|
import { NumberComponent, NumberObject } from "./NumberComponent";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { SerializedValue } from './GenericComponent';
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
import { QuantityMap } from "../types/QuantityMap";
|
||||||
|
|
||||||
type SliderComponentProps = {
|
type SliderComponentProps = {
|
||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
@ -12,11 +13,11 @@ type SliderComponentProps = {
|
|||||||
max: NumberObject;
|
max: NumberObject;
|
||||||
value: NumberObject;
|
value: NumberObject;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
docString: string;
|
docString: string | null;
|
||||||
stepSize: NumberObject;
|
stepSize: NumberObject;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
|
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@ -35,7 +36,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
addNotification,
|
addNotification,
|
||||||
changeCallback = () => {},
|
changeCallback = () => {},
|
||||||
displayName,
|
displayName,
|
||||||
id
|
id,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -58,44 +59,76 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
addNotification(`${fullAccessPath}.stepSize changed to ${stepSize.value}.`);
|
addNotification(`${fullAccessPath}.stepSize changed to ${stepSize.value}.`);
|
||||||
}, [props.stepSize]);
|
}, [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
|
// This will never be the case as we do not have a range slider. However, we should
|
||||||
// make sure this is properly handled.
|
// make sure this is properly handled.
|
||||||
if (Array.isArray(newNumber)) {
|
if (Array.isArray(newNumber)) {
|
||||||
newNumber = newNumber[0];
|
newNumber = newNumber[0];
|
||||||
}
|
}
|
||||||
changeCallback({
|
|
||||||
|
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,
|
type: value.type,
|
||||||
value: newNumber,
|
value: newNumber,
|
||||||
full_access_path: `${fullAccessPath}.value`,
|
full_access_path: `${fullAccessPath}.value`,
|
||||||
readonly: value.readonly,
|
readonly: value.readonly,
|
||||||
doc: docString
|
doc: docString,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
changeCallback(serializedObject);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleValueChange = (
|
const handleValueChange = (
|
||||||
newValue: number,
|
newValue: number,
|
||||||
name: string,
|
name: string,
|
||||||
valueObject: NumberObject
|
valueObject: NumberObject,
|
||||||
) => {
|
) => {
|
||||||
changeCallback({
|
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,
|
type: valueObject.type,
|
||||||
value: newValue,
|
value: newValue,
|
||||||
full_access_path: `${fullAccessPath}.${name}`,
|
full_access_path: `${fullAccessPath}.${name}`,
|
||||||
readonly: valueObject.readonly
|
readonly: valueObject.readonly,
|
||||||
});
|
doc: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
changeCallback(serializedObject);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deconstructNumberDict = (
|
const deconstructNumberDict = (
|
||||||
numberDict: NumberObject
|
numberDict: NumberObject,
|
||||||
): [number, boolean, string | null] => {
|
): [number, boolean, string | undefined] => {
|
||||||
let numberMagnitude: number;
|
let numberMagnitude: number = 0;
|
||||||
let numberUnit: string | null = null;
|
let numberUnit: string | undefined = undefined;
|
||||||
const numberReadOnly = numberDict.readonly;
|
const numberReadOnly = numberDict.readonly;
|
||||||
|
|
||||||
if (numberDict.type === 'int' || numberDict.type === 'float') {
|
if (numberDict.type === "int" || numberDict.type === "float") {
|
||||||
numberMagnitude = numberDict.value;
|
numberMagnitude = numberDict.value;
|
||||||
} else if (numberDict.type === 'Quantity') {
|
} else if (numberDict.type === "Quantity") {
|
||||||
numberMagnitude = numberDict.value.magnitude;
|
numberMagnitude = numberDict.value.magnitude;
|
||||||
numberUnit = numberDict.value.unit;
|
numberUnit = numberDict.value.unit;
|
||||||
}
|
}
|
||||||
@ -110,7 +143,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="component sliderComponent" id={id}>
|
<div className="component sliderComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -123,7 +156,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs="5" xl>
|
<Col xs="5" xl>
|
||||||
<Slider
|
<Slider
|
||||||
style={{ margin: '0px 0px 10px 0px' }}
|
style={{ margin: "0px 0px 10px 0px" }}
|
||||||
aria-label="Always visible"
|
aria-label="Always visible"
|
||||||
// valueLabelDisplay="on"
|
// valueLabelDisplay="on"
|
||||||
disabled={valueReadOnly}
|
disabled={valueReadOnly}
|
||||||
@ -134,7 +167,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
step={stepSizeMagnitude}
|
step={stepSizeMagnitude}
|
||||||
marks={[
|
marks={[
|
||||||
{ value: minMagnitude, label: `${minMagnitude}` },
|
{ value: minMagnitude, label: `${minMagnitude}` },
|
||||||
{ value: maxMagnitude, label: `${maxMagnitude}` }
|
{ value: maxMagnitude, label: `${maxMagnitude}` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -144,12 +177,12 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
fullAccessPath={`${fullAccessPath}.value`}
|
fullAccessPath={`${fullAccessPath}.value`}
|
||||||
docString={docString}
|
docString={docString}
|
||||||
readOnly={valueReadOnly}
|
readOnly={valueReadOnly}
|
||||||
type="float"
|
type={value.type}
|
||||||
value={valueMagnitude}
|
value={valueMagnitude}
|
||||||
unit={valueUnit}
|
unit={valueUnit}
|
||||||
addNotification={() => {}}
|
addNotification={() => {}}
|
||||||
changeCallback={changeCallback}
|
changeCallback={changeCallback}
|
||||||
id={id + '-value'}
|
id={id + "-value"}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
@ -179,14 +212,14 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Row
|
<Row
|
||||||
className="justify-content-center"
|
className="justify-content-center"
|
||||||
style={{ paddingTop: '20px', margin: '10px' }}>
|
style={{ paddingTop: "20px", margin: "10px" }}>
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Form.Label>Min Value</Form.Label>
|
<Form.Label>Min Value</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="number"
|
type="number"
|
||||||
value={minMagnitude}
|
value={minMagnitude}
|
||||||
disabled={minReadOnly}
|
disabled={minReadOnly}
|
||||||
onChange={(e) => handleValueChange(Number(e.target.value), 'min', min)}
|
onChange={(e) => handleValueChange(Number(e.target.value), "min", min)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
@ -196,7 +229,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
type="number"
|
type="number"
|
||||||
value={maxMagnitude}
|
value={maxMagnitude}
|
||||||
disabled={maxReadOnly}
|
disabled={maxReadOnly}
|
||||||
onChange={(e) => handleValueChange(Number(e.target.value), 'max', max)}
|
onChange={(e) => handleValueChange(Number(e.target.value), "max", max)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
@ -207,7 +240,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
value={stepSizeMagnitude}
|
value={stepSizeMagnitude}
|
||||||
disabled={stepSizeReadOnly}
|
disabled={stepSizeReadOnly}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleValueChange(Number(e.target.value), 'step_size', stepSize)
|
handleValueChange(Number(e.target.value), "step_size", stepSize)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -217,3 +250,5 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SliderComponent.displayName = "SliderComponent";
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Form, InputGroup } from 'react-bootstrap';
|
import { Form, InputGroup } from "react-bootstrap";
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from "./DocStringComponent";
|
||||||
import '../App.css';
|
import "../App.css";
|
||||||
import { LevelName } from './NotificationsComponent';
|
import { LevelName } from "./NotificationsComponent";
|
||||||
import { SerializedValue } from './GenericComponent';
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
|
||||||
// TODO: add button functionality
|
// TODO: add button functionality
|
||||||
|
|
||||||
@ -11,10 +11,10 @@ type StringComponentProps = {
|
|||||||
fullAccessPath: string;
|
fullAccessPath: string;
|
||||||
value: string;
|
value: string;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
docString: string;
|
docString: string | null;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
addNotification: (message: string, levelname?: LevelName) => void;
|
addNotification: (message: string, levelname?: LevelName) => void;
|
||||||
changeCallback?: (value: SerializedValue, callback?: (ack: unknown) => void) => void;
|
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@ -28,7 +28,7 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
|
|||||||
addNotification,
|
addNotification,
|
||||||
changeCallback = () => {},
|
changeCallback = () => {},
|
||||||
displayName,
|
displayName,
|
||||||
id
|
id,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
@ -46,27 +46,27 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
|
|||||||
addNotification(`${fullAccessPath} changed to ${props.value}.`);
|
addNotification(`${fullAccessPath} changed to ${props.value}.`);
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
const handleChange = (event) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setInputString(event.target.value);
|
setInputString(event.target.value);
|
||||||
if (isInstantUpdate) {
|
if (isInstantUpdate) {
|
||||||
changeCallback({
|
changeCallback({
|
||||||
type: 'str',
|
type: "str",
|
||||||
value: event.target.value,
|
value: event.target.value,
|
||||||
full_access_path: fullAccessPath,
|
full_access_path: fullAccessPath,
|
||||||
readonly: readOnly,
|
readonly: readOnly,
|
||||||
doc: docString
|
doc: docString,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === 'Enter' && !isInstantUpdate) {
|
if (event.key === "Enter" && !isInstantUpdate) {
|
||||||
changeCallback({
|
changeCallback({
|
||||||
type: 'str',
|
type: "str",
|
||||||
value: inputString,
|
value: inputString,
|
||||||
full_access_path: fullAccessPath,
|
full_access_path: fullAccessPath,
|
||||||
readonly: readOnly,
|
readonly: readOnly,
|
||||||
doc: docString
|
doc: docString,
|
||||||
});
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@ -75,18 +75,18 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
|
|||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
if (!isInstantUpdate) {
|
if (!isInstantUpdate) {
|
||||||
changeCallback({
|
changeCallback({
|
||||||
type: 'str',
|
type: "str",
|
||||||
value: inputString,
|
value: inputString,
|
||||||
full_access_path: fullAccessPath,
|
full_access_path: fullAccessPath,
|
||||||
readonly: readOnly,
|
readonly: readOnly,
|
||||||
doc: docString
|
doc: docString,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="component stringComponent" id={id}>
|
<div className="component stringComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div>Render count: {renderCount.current}</div>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
@ -102,9 +102,11 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
|
className={isInstantUpdate && !readOnly ? "instantUpdate" : ""}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
StringComponent.displayName = "StringComponent";
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import App from './App';
|
import App from "./App";
|
||||||
import { createRoot } from 'react-dom/client';
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
|
||||||
// Importing the Bootstrap CSS
|
// 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
|
// Render the App component into the #root div
|
||||||
const container = document.getElementById('root');
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
const root = createRoot(container);
|
<React.StrictMode>
|
||||||
root.render(<App />);
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
import { io } from 'socket.io-client';
|
import { io } from "socket.io-client";
|
||||||
import { SerializedValue } from './components/GenericComponent';
|
import { serializeDict, serializeList } from "./utils/serializationUtils";
|
||||||
import { serializeDict, serializeList } from './utils/serializationUtils';
|
import { SerializedObject } from "./types/SerializedObject";
|
||||||
|
|
||||||
export const hostname =
|
export const hostname =
|
||||||
process.env.NODE_ENV === 'development' ? `localhost` : window.location.hostname;
|
process.env.NODE_ENV === "development" ? `localhost` : window.location.hostname;
|
||||||
export const port =
|
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}/`;
|
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 = (
|
export const updateValue = (
|
||||||
serializedObject: SerializedValue,
|
serializedObject: SerializedObject,
|
||||||
callback?: (ack: unknown) => void
|
callback?: (ack: unknown) => void,
|
||||||
) => {
|
) => {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
socket.emit(
|
socket.emit(
|
||||||
'update_value',
|
"update_value",
|
||||||
{ access_path: serializedObject['full_access_path'], value: serializedObject },
|
{ access_path: serializedObject["full_access_path"], value: serializedObject },
|
||||||
callback
|
callback,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
socket.emit('update_value', {
|
socket.emit("update_value", {
|
||||||
access_path: serializedObject['full_access_path'],
|
access_path: serializedObject["full_access_path"],
|
||||||
value: serializedObject
|
value: serializedObject,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -33,22 +33,22 @@ export const runMethod = (
|
|||||||
accessPath: string,
|
accessPath: string,
|
||||||
args: unknown[] = [],
|
args: unknown[] = [],
|
||||||
kwargs: Record<string, unknown> = {},
|
kwargs: Record<string, unknown> = {},
|
||||||
callback?: (ack: unknown) => void
|
callback?: (ack: unknown) => void,
|
||||||
) => {
|
) => {
|
||||||
const serializedArgs = serializeList(args);
|
const serializedArgs = serializeList(args);
|
||||||
const serializedKwargs = serializeDict(kwargs);
|
const serializedKwargs = serializeDict(kwargs);
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
socket.emit(
|
socket.emit(
|
||||||
'trigger_method',
|
"trigger_method",
|
||||||
{ access_path: accessPath, args: serializedArgs, kwargs: serializedKwargs },
|
{ access_path: accessPath, args: serializedArgs, kwargs: serializedKwargs },
|
||||||
callback
|
callback,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
socket.emit('trigger_method', {
|
socket.emit("trigger_method", {
|
||||||
access_path: accessPath,
|
access_path: accessPath,
|
||||||
args: serializedArgs,
|
args: serializedArgs,
|
||||||
kwargs: serializedKwargs
|
kwargs: serializedKwargs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
4
frontend/src/types/QuantityMap.ts
Normal file
4
frontend/src/types/QuantityMap.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type QuantityMap = {
|
||||||
|
magnitude: number;
|
||||||
|
unit: string;
|
||||||
|
};
|
101
frontend/src/types/SerializedObject.ts
Normal file
101
frontend/src/types/SerializedObject.ts
Normal 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;
|
@ -1,101 +1,100 @@
|
|||||||
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
|
||||||
const serializePrimitive = (
|
const serializePrimitive = (
|
||||||
obj: number | boolean | string | null,
|
obj: number | boolean | string | null,
|
||||||
accessPath: string
|
accessPath: string,
|
||||||
) => {
|
): SerializedObject => {
|
||||||
let type: string;
|
if (typeof obj === "number") {
|
||||||
|
|
||||||
if (typeof obj === 'number') {
|
|
||||||
type = Number.isInteger(obj) ? 'int' : 'float';
|
|
||||||
return {
|
return {
|
||||||
full_access_path: accessPath,
|
full_access_path: accessPath,
|
||||||
doc: null,
|
doc: null,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
type,
|
type: Number.isInteger(obj) ? "int" : "float",
|
||||||
value: obj
|
value: obj,
|
||||||
};
|
};
|
||||||
} else if (typeof obj === 'boolean') {
|
} else if (typeof obj === "boolean") {
|
||||||
type = 'bool';
|
|
||||||
return {
|
return {
|
||||||
full_access_path: accessPath,
|
full_access_path: accessPath,
|
||||||
doc: null,
|
doc: null,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
type,
|
type: "bool",
|
||||||
value: obj
|
value: obj,
|
||||||
};
|
};
|
||||||
} else if (typeof obj === 'string') {
|
} else if (typeof obj === "string") {
|
||||||
type = 'str';
|
|
||||||
return {
|
return {
|
||||||
full_access_path: accessPath,
|
full_access_path: accessPath,
|
||||||
doc: null,
|
doc: null,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
type,
|
type: "str",
|
||||||
value: obj
|
value: obj,
|
||||||
};
|
};
|
||||||
} else if (obj === null) {
|
} else if (obj === null) {
|
||||||
type = 'NoneType';
|
|
||||||
return {
|
return {
|
||||||
full_access_path: accessPath,
|
full_access_path: accessPath,
|
||||||
doc: null,
|
doc: null,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
type,
|
type: "None",
|
||||||
value: null
|
value: null,
|
||||||
};
|
};
|
||||||
} else {
|
} 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 doc = null;
|
||||||
const value = obj.map((item, index) => {
|
const value = obj.map((item, index) => {
|
||||||
if (
|
if (
|
||||||
typeof item === 'number' ||
|
typeof item === "number" ||
|
||||||
typeof item === 'boolean' ||
|
typeof item === "boolean" ||
|
||||||
typeof item === 'string' ||
|
typeof item === "string" ||
|
||||||
item === null
|
item === null
|
||||||
) {
|
) {
|
||||||
serializePrimitive(
|
serializePrimitive(
|
||||||
item as number | boolean | string | null,
|
item as number | boolean | string | null,
|
||||||
`${accessPath}[${index}]`
|
`${accessPath}[${index}]`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
full_access_path: accessPath,
|
full_access_path: accessPath,
|
||||||
type: 'list',
|
type: "list",
|
||||||
value,
|
value,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
doc
|
doc,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export const serializeDict = (
|
export const serializeDict = (
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
accessPath: string = ''
|
accessPath: string = "",
|
||||||
) => {
|
) => {
|
||||||
const doc = null;
|
const doc = null;
|
||||||
const value = Object.entries(obj).reduce((acc, [key, val]) => {
|
const value = Object.entries(obj).reduce(
|
||||||
|
(acc, [key, val]) => {
|
||||||
// Construct the new access path for nested properties
|
// Construct the new access path for nested properties
|
||||||
const newPath = `${accessPath}["${key}"]`;
|
const newPath = `${accessPath}["${key}"]`;
|
||||||
|
|
||||||
// Serialize each value in the dictionary and assign to the accumulator
|
// Serialize each value in the dictionary and assign to the accumulator
|
||||||
if (
|
if (
|
||||||
typeof val === 'number' ||
|
typeof val === "number" ||
|
||||||
typeof val === 'boolean' ||
|
typeof val === "boolean" ||
|
||||||
typeof val === 'string' ||
|
typeof val === "string" ||
|
||||||
val === null
|
val === null
|
||||||
) {
|
) {
|
||||||
acc[key] = serializePrimitive(val as number | boolean | string | null, newPath);
|
acc[key] = serializePrimitive(val as number | boolean | string | null, newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
},
|
||||||
|
<Record<string, SerializedObject>>{},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
full_access_path: accessPath,
|
full_access_path: accessPath,
|
||||||
type: 'dict',
|
type: "dict",
|
||||||
value,
|
value,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
doc
|
doc,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { SerializedValue } from '../components/GenericComponent';
|
import { SerializedObject } from "../types/SerializedObject";
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
value: Record<string, SerializedValue> | null;
|
value: Record<string, SerializedObject> | null;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
doc: string | null;
|
doc: string | null;
|
||||||
};
|
};
|
||||||
@ -45,7 +45,7 @@ export function parseFullAccessPath(path: string): string[] {
|
|||||||
*/
|
*/
|
||||||
function parseSerializedKey(serializedKey: string): string | number {
|
function parseSerializedKey(serializedKey: string): string | number {
|
||||||
// Strip outer brackets if present
|
// Strip outer brackets if present
|
||||||
if (serializedKey.startsWith('[') && serializedKey.endsWith(']')) {
|
if (serializedKey.startsWith("[") && serializedKey.endsWith("]")) {
|
||||||
serializedKey = serializedKey.slice(1, -1);
|
serializedKey = serializedKey.slice(1, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,12 +68,13 @@ function parseSerializedKey(serializedKey: string): string | number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateItemInContainer(
|
function getOrCreateItemInContainer(
|
||||||
container: Record<string | number, SerializedValue> | SerializedValue[],
|
container: Record<string | number, SerializedObject> | SerializedObject[],
|
||||||
key: string | number,
|
key: string | number,
|
||||||
allowAddKey: boolean
|
allowAddKey: boolean,
|
||||||
): SerializedValue {
|
): SerializedObject {
|
||||||
// Check if the key exists and return the item if it does
|
// Check if the key exists and return the item if it does
|
||||||
if (key in container) {
|
if (key in container) {
|
||||||
|
/* @ts-expect-error Key is in the correct form but converted to type any for some reason */
|
||||||
return container[key];
|
return container[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,10 +108,10 @@ function getOrCreateItemInContainer(
|
|||||||
* @throws SerializationValueError If the expected structure is incorrect.
|
* @throws SerializationValueError If the expected structure is incorrect.
|
||||||
*/
|
*/
|
||||||
function getContainerItemByKey(
|
function getContainerItemByKey(
|
||||||
container: Record<string, SerializedValue> | SerializedValue[],
|
container: Record<string, SerializedObject> | SerializedObject[],
|
||||||
key: string,
|
key: string,
|
||||||
allowAppend: boolean = false
|
allowAppend: boolean = false,
|
||||||
): SerializedValue {
|
): SerializedObject {
|
||||||
const processedKey = parseSerializedKey(key);
|
const processedKey = parseSerializedKey(key);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -126,13 +127,13 @@ function getContainerItemByKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setNestedValueByPath(
|
export function setNestedValueByPath(
|
||||||
serializationDict: Record<string, SerializedValue>,
|
serializationDict: Record<string, SerializedObject>,
|
||||||
path: string,
|
path: string,
|
||||||
serializedValue: SerializedValue
|
serializedValue: SerializedObject,
|
||||||
): Record<string, SerializedValue> {
|
): Record<string, SerializedObject> {
|
||||||
const pathParts = parseFullAccessPath(path);
|
const pathParts = parseFullAccessPath(path);
|
||||||
const newSerializationDict: Record<string, SerializedValue> = JSON.parse(
|
const newSerializationDict: Record<string, SerializedObject> = JSON.parse(
|
||||||
JSON.stringify(serializationDict)
|
JSON.stringify(serializationDict),
|
||||||
);
|
);
|
||||||
|
|
||||||
let currentDict = newSerializationDict;
|
let currentDict = newSerializationDict;
|
||||||
@ -143,11 +144,11 @@ export function setNestedValueByPath(
|
|||||||
const nextLevelSerializedObject = getContainerItemByKey(
|
const nextLevelSerializedObject = getContainerItemByKey(
|
||||||
currentDict,
|
currentDict,
|
||||||
pathPart,
|
pathPart,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
currentDict = nextLevelSerializedObject['value'] as Record<
|
currentDict = nextLevelSerializedObject["value"] as Record<
|
||||||
string,
|
string,
|
||||||
SerializedValue
|
SerializedObject
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,14 +161,15 @@ export function setNestedValueByPath(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error occurred trying to change ${path}: ${error}`);
|
console.error(`Error occurred trying to change ${path}: ${error}`);
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptySerializedObject(): SerializedValue {
|
function createEmptySerializedObject(): SerializedObject {
|
||||||
return {
|
return {
|
||||||
full_access_path: '',
|
full_access_path: "",
|
||||||
value: undefined,
|
value: null,
|
||||||
type: 'None',
|
type: "None",
|
||||||
doc: null,
|
doc: null,
|
||||||
readonly: false
|
readonly: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
export function getIdFromFullAccessPath(fullAccessPath: string) {
|
export function getIdFromFullAccessPath(fullAccessPath: string) {
|
||||||
if (fullAccessPath) {
|
if (fullAccessPath) {
|
||||||
// Replace '].' with a single dash
|
// 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
|
// 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
|
// Remove any trailing dashes
|
||||||
id = id.replace(/-+$/, '');
|
id = id.replace(/-+$/, "");
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
} else {
|
} else {
|
||||||
return 'main';
|
return "main";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
frontend/tsconfig.app.json
Normal file
31
frontend/tsconfig.app.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"files": [],
|
||||||
"jsx": "react-jsx",
|
"references": [
|
||||||
"allowImportingTsExtensions": true,
|
{
|
||||||
"noEmit": true,
|
"path": "./tsconfig.app.json"
|
||||||
"esModuleInterop": true
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
15
frontend/tsconfig.node.json
Normal file
15
frontend/tsconfig.node.json
Normal 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
13
frontend/vite.config.ts
Normal 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"],
|
||||||
|
},
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user