From c9aee80a636037fee4fc2a99081a54f6805b7169 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Wed, 3 Apr 2024 16:14:57 +0200 Subject: [PATCH 01/14] Update deprecated `webpack.config.js` * `npm run dev` did not succeed anymore, so the `contentBase` needed to be changed to `static`. * Deprecated `--watch` option is now removed from `dev` script. --- src/Moryx.CommandCenter.Web/package.json | 2 +- src/Moryx.CommandCenter.Web/webpack.config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index f5b69d224..00b85caf5 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "start": "npm dev", - "dev": "webpack-dev-server --watch --progress --mode development --config webpack.dev.config.js", + "dev": "webpack-dev-server --progress --mode development --config webpack.dev.config.js", "test": "echo \"Error: no test specified\" && exit 1", "build": "npm install && rimraf wwwroot/*.js && webpack --mode production --config webpack.prod.config.js" }, diff --git a/src/Moryx.CommandCenter.Web/webpack.config.js b/src/Moryx.CommandCenter.Web/webpack.config.js index e7b4c6bdf..e16eb3635 100644 --- a/src/Moryx.CommandCenter.Web/webpack.config.js +++ b/src/Moryx.CommandCenter.Web/webpack.config.js @@ -17,7 +17,7 @@ module.exports = (env, options) => { }, devServer: { - contentBase: __dirname + "/wwwroot" + static: __dirname + "/wwwroot" }, resolve: { From c767c457f16c673a3e605e4eedf24054cd4149d1 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Fri, 5 Apr 2024 15:03:22 +0200 Subject: [PATCH 02/14] Use `sass` instead of deprecated `node-sass` --- src/Moryx.CommandCenter.Web/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index 00b85caf5..a53bf2728 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -41,7 +41,7 @@ "@types/react-router-redux": "^5.0.27", "css-loader": "^6.10.0", "html-webpack-plugin": "^5.6.0", - "node-sass": "^9.0.0", + "sass": "^1.72.0", "sass-loader": "^14.1.0", "source-map-loader": "^5.0.0", "style-loader": "^3.3.4", @@ -50,9 +50,9 @@ "tslint-react": "^5.0.0", "typescript": "^5.3.3", "url-loader": "^4.1.1", - "webpack": "^5.90.2", + "webpack": "^5.91.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.2", "webpack-merge": "^5.10.0" } -} \ No newline at end of file +} From 55758b1ba0a7d6d8a6364d3cd66cdc7df32d40b2 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:06:11 +0200 Subject: [PATCH 03/14] Add rimraf as dev dependency It was missing and lead to errors when trying to build the app. --- src/Moryx.CommandCenter.Web/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index a53bf2728..18bbf5e6b 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -7,7 +7,7 @@ "start": "npm dev", "dev": "webpack-dev-server --progress --mode development --config webpack.dev.config.js", "test": "echo \"Error: no test specified\" && exit 1", - "build": "npm install && rimraf wwwroot/*.js && webpack --mode production --config webpack.prod.config.js" + "build": "npm install && rimraf --glob wwwroot/*.js && webpack --mode development --config webpack.prod.config.js" }, "author": "mma", "license": "Apache-2.0", @@ -21,6 +21,7 @@ "bootstrap": "5.3.3", "bootstrap5-toggle": "^5.0.6", "moment": "^2.30.1", + "path-scurry": "^1.10.2", "query-string": "^9.0.0", "react": "18.2.0", "react-bootstrap-toggle": "^2.3.2", @@ -41,6 +42,7 @@ "@types/react-router-redux": "^5.0.27", "css-loader": "^6.10.0", "html-webpack-plugin": "^5.6.0", + "rimraf": "5.0.5", "sass": "^1.72.0", "sass-loader": "^14.1.0", "source-map-loader": "^5.0.0", From 9d80172c38b2e9a09b21eb720551b4735e44356e Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:08:53 +0200 Subject: [PATCH 04/14] Add `index.html` to debug the app using `npm run dev` --- src/Moryx.CommandCenter.Web/src/index.html | 9 +++++++++ src/Moryx.CommandCenter.Web/webpack.dev.config.js | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/Moryx.CommandCenter.Web/src/index.html diff --git a/src/Moryx.CommandCenter.Web/src/index.html b/src/Moryx.CommandCenter.Web/src/index.html new file mode 100644 index 000000000..b735fb42b --- /dev/null +++ b/src/Moryx.CommandCenter.Web/src/index.html @@ -0,0 +1,9 @@ + + + + + CommandCenter + + +
+ \ No newline at end of file diff --git a/src/Moryx.CommandCenter.Web/webpack.dev.config.js b/src/Moryx.CommandCenter.Web/webpack.dev.config.js index c9d565cf2..ff5a0a541 100644 --- a/src/Moryx.CommandCenter.Web/webpack.dev.config.js +++ b/src/Moryx.CommandCenter.Web/webpack.dev.config.js @@ -1,12 +1,21 @@ const webpack = require('webpack'); const { merge } = require('webpack-merge'); const baseConfig = require('./webpack.config.js'); +const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = (env, options) => merge(baseConfig(env, options), { devtool: "inline-source-map", + devServer: { + port: 4200 + }, plugins: [ new webpack.DefinePlugin({ - "BASE_URL": JSON.stringify('http://localhost:5000'), + "BASE_URL": JSON.stringify('https://localhost:5000'), + }), + new HtmlWebpackPlugin({ + template: __dirname + '/src/index.html', + filename: 'index.html', + inject: 'body' }) - ] + ], }); From e429862ec627731d495b2937e6d20ba109143116 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:10:55 +0200 Subject: [PATCH 05/14] Add `@mui/material` in order to replace `reactstrap`/`bootstrap` --- src/Moryx.CommandCenter.Web/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index 18bbf5e6b..9f7586c74 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -12,8 +12,11 @@ "author": "mma", "license": "Apache-2.0", "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", + "@mui/material": "^5.15.15", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "@types/react-redux": "^7.1.33", From 6f897f5d2123328aef5a4a010fba0066e3c7d63d Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:17:12 +0200 Subject: [PATCH 06/14] Update `RoutingMenu` to use Material UI These changes also include a fix for where the previous version could mark multiple list items as active if they were similar (starting with same string). Now, the path is checked for being a sub path, rather than just starting with the same string. Also removes unused `TreeMenu`. --- .../common/components/Menu/RoutingMenu.tsx | 27 +++--- .../components/Menu/RoutingMenuItem.tsx | 52 ++++++---- .../src/common/components/Menu/TreeMenu.tsx | 45 --------- .../common/components/Menu/TreeMenuItem.tsx | 95 ------------------- .../src/common/models/MenuItemModel.ts | 1 + .../src/common/models/MenuModel.ts | 5 + 6 files changed, 53 insertions(+), 172 deletions(-) delete mode 100644 src/Moryx.CommandCenter.Web/src/common/components/Menu/TreeMenu.tsx delete mode 100644 src/Moryx.CommandCenter.Web/src/common/components/Menu/TreeMenuItem.tsx diff --git a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx index 87bfa4be3..edfedd664 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx @@ -3,11 +3,12 @@ * Licensed under the Apache License, Version 2.0 */ +import List from "@mui/material/List"; import * as React from "react"; import { useNavigate } from "react-router-dom"; import MenuItemModel from "../../models/MenuItemModel"; +import { MenuProps } from "../../models/MenuModel"; import RoutingMenuItem from "./RoutingMenuItem"; -import { MenuProps } from "./TreeMenu"; function RoutingMenu(props: MenuProps) { const navigate = useNavigate(); @@ -20,16 +21,20 @@ function RoutingMenu(props: MenuProps) { }; const renderMenu = (menuItems: MenuItemModel[]): React.ReactNode => { - return menuItems.map((menuItem, idx) => { - return ( - handleMenuItemClick(menuItem)} - /> - ); - }); + return { + menuItems.map((menuItem, idx) => { + return ( + handleMenuItemClick(menuItem)} + /> + ); + }) + } ; }; return
{renderMenu(props.Menu.MenuItems)}
; diff --git a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx index 0cbc665ae..250a19314 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx @@ -3,38 +3,30 @@ * Licensed under the Apache License, Version 2.0 */ +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; import * as React from "react"; -import { Link, Location, useLocation, useNavigate } from "react-router-dom"; -import { ListGroupItem } from "reactstrap"; +import { Location, useLocation, useNavigate } from "react-router-dom"; import MenuItemModel from "../../models/MenuItemModel"; interface MenuItemProps { + Key: number; MenuItem: MenuItemModel; Level: number; + Divider: boolean; onMenuItemClicked?(menuItem: MenuItemModel): void; } -interface MenuItemState { - IsOpened: boolean; -} - function RoutingMenuItem(props: MenuItemProps) { const location = useLocation(); const navigate = useNavigate(); - const isOpened = (location: Location): boolean => { - return location.pathname.startsWith(props.MenuItem.NavPath); - }; - - const [IsOpened, setIsOpened] = React.useState(isOpened(location)); - React.useEffect(() => { - setIsOpened(isOpened(location)); }, [navigate]); const handleMenuItemClick = (e: React.MouseEvent): void => { e.preventDefault(); - setIsOpened((prevState) => !prevState); onMenuItemClicked(props.MenuItem); }; @@ -44,15 +36,33 @@ function RoutingMenuItem(props: MenuItemProps) { } }; - const isActive = isOpened(location); + const isActive = (location: Location): boolean => { + // Path has to be equal to be 'active' or must be a sub path (following + // After a `/`). Otherwise, with similar entries, multiple list items + // Could be highlighted. E.g.: 'Orders' and 'OrdersSimulator' would both + // Match the condition of `OrdersSimulator.startsWith(Orders)`. + return location.pathname === props.MenuItem.NavPath + || (location.pathname.startsWith(props.MenuItem.NavPath) + && location.pathname.replace(props.MenuItem.NavPath, "")[0] === "/"); + }; + + const isLocationActive = isActive(location); return ( - ) => handleMenuItemClick(e)}> - - {props.MenuItem.Name} - - {props.MenuItem.Content} - + + ) => handleMenuItemClick(e)} + divider={props.Divider} + > + + {props.MenuItem.Content} + + + ); } diff --git a/src/Moryx.CommandCenter.Web/src/common/components/Menu/TreeMenu.tsx b/src/Moryx.CommandCenter.Web/src/common/components/Menu/TreeMenu.tsx deleted file mode 100644 index 3fd12bb62..000000000 --- a/src/Moryx.CommandCenter.Web/src/common/components/Menu/TreeMenu.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import * as React from "react"; -import { Collapse } from "reactstrap"; -import MenuItemModel from "../../models/MenuItemModel"; -import MenuModel from "../../models/MenuModel"; -import TreeMenuItem from "./TreeMenuItem"; - -export interface MenuProps { - Menu: MenuModel; - onActiveMenuItemChanged?(menuItem: MenuItemModel): void; -} - -export default class TreeMenu extends React.Component { - - constructor(props: MenuProps) { - super (props); - this.state = {}; - } - - protected handleMenuItemClick(menuItem: MenuItemModel): void { - if (this.props.onActiveMenuItemChanged != null) { - this.props.onActiveMenuItemChanged(menuItem); - } - } - - protected renderMenu(menuItems: MenuItemModel[]): React.ReactNode { - return menuItems.map ((menuItem, idx) => { - return ( - - ); - }); - } - - public render(): React.ReactNode { - return ( -
- {this.renderMenu(this.props.Menu.MenuItems)} -
- ); - } -} diff --git a/src/Moryx.CommandCenter.Web/src/common/components/Menu/TreeMenuItem.tsx b/src/Moryx.CommandCenter.Web/src/common/components/Menu/TreeMenuItem.tsx deleted file mode 100644 index 3165247e4..000000000 --- a/src/Moryx.CommandCenter.Web/src/common/components/Menu/TreeMenuItem.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import { mdiChevronDown, mdiChevronUp } from "@mdi/js"; -import Icon from "@mdi/react"; -import * as React from "react"; -import { Col, Collapse, Container, Row } from "reactstrap"; -import MenuItemModel, { IconType } from "../../models/MenuItemModel"; - -interface MenuItemProps { - MenuItem: MenuItemModel; - Level: number; - onMenuItemClicked?(menuItem: MenuItemModel): void; -} - -interface MenuItemState { - IsOpened: boolean; -} - -export default class TreeMenuItem extends React.Component { - constructor(props: MenuItemProps) { - super(props); - this.state = { IsOpened: false }; - - this.onMenuItemClicked = this.onMenuItemClicked.bind(this); - } - - private handleMenuItemClick(e: React.MouseEvent): void { - e.preventDefault(); - - this.setState((prevState) => ({ IsOpened: !prevState.IsOpened })); - this.onMenuItemClicked(this.props.MenuItem); - } - - private onMenuItemClicked(menuItem: MenuItemModel): void { - if (this.props.onMenuItemClicked != null) { - this.props.onMenuItemClicked(menuItem); - } - } - - private renderSubMenuItems(): React.ReactNode { - return this.props.MenuItem.SubMenuItems.map((menuItem, idx) => - ); - } - - public render(): React.ReactNode { - const hasSubItems = this.props.MenuItem.SubMenuItems.length > 0; - const iconType = this.props.MenuItem.IconType == undefined ? IconType.Icon : IconType.Image; - const defaultContent = ( -
- {this.props.MenuItem.Icon !== undefined && iconType === IconType.Icon && - - } - {this.props.MenuItem.Icon !== undefined && iconType === IconType.Image && - - } - - {this.props.MenuItem.Name} -
- ); - - return ( -
- - - -
- {this.props.MenuItem.Content === undefined && - defaultContent - } - {this.props.MenuItem.Content !== undefined && - this.props.MenuItem.Content - } -
- - ) => this.handleMenuItemClick(e)}> - {hasSubItems && - - } - -
-
- - {this.renderSubMenuItems()} - -
- ); - } -} diff --git a/src/Moryx.CommandCenter.Web/src/common/models/MenuItemModel.ts b/src/Moryx.CommandCenter.Web/src/common/models/MenuItemModel.ts index af264506e..ffeaf97b2 100644 --- a/src/Moryx.CommandCenter.Web/src/common/models/MenuItemModel.ts +++ b/src/Moryx.CommandCenter.Web/src/common/models/MenuItemModel.ts @@ -11,6 +11,7 @@ export const enum IconType { } export default interface MenuItemModel { + Secondary?: string; Name: string; NavPath: string; SubMenuItems: MenuItemModel[]; diff --git a/src/Moryx.CommandCenter.Web/src/common/models/MenuModel.ts b/src/Moryx.CommandCenter.Web/src/common/models/MenuModel.ts index 44ee54658..da6ceb6c8 100644 --- a/src/Moryx.CommandCenter.Web/src/common/models/MenuModel.ts +++ b/src/Moryx.CommandCenter.Web/src/common/models/MenuModel.ts @@ -5,6 +5,11 @@ import MenuItemModel from "./MenuItemModel"; +export interface MenuProps { + Menu: MenuModel; + onActiveMenuItemChanged?(menuItem: MenuItemModel): void; +} + export default interface MenuModel { MenuItems: MenuItemModel[]; } From eec368db9598bad15e4988acb639c462008069c7 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:18:46 +0200 Subject: [PATCH 07/14] Make use of Material UI by providing it to the app --- .../src/common/container/App.tsx | 9 ++++----- src/Moryx.CommandCenter.Web/src/index.tsx | 20 ++++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Moryx.CommandCenter.Web/src/common/container/App.tsx b/src/Moryx.CommandCenter.Web/src/common/container/App.tsx index bd24fae0c..1fe1e8c8a 100644 --- a/src/Moryx.CommandCenter.Web/src/common/container/App.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/container/App.tsx @@ -3,12 +3,12 @@ * Licensed under the Apache License, Version 2.0 */ +import Container from "@mui/material/Container"; import * as React from "react"; import { connect } from "react-redux"; import { Navigate, Route, Routes } from "react-router-dom"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; -import { Container } from "reactstrap"; import DatabasesRestClient from "../../databases/api/DatabasesRestClient"; import Databases from "../../databases/container/Databases"; import ModulesRestClient from "../../modules/api/ModulesRestClient"; @@ -25,7 +25,6 @@ import SystemLoadResponse from "../api/responses/SystemLoadResponse"; import { AppState } from "../redux/AppState"; import { updateIsConnected, updateServerTime } from "../redux/CommonActions"; import { ActionType } from "../redux/Types"; -import "../scss/commandcenter.scss"; interface AppPropModel { ModulesRestClient: ModulesRestClient; @@ -97,11 +96,11 @@ function App(props: AppPropModel & AppDispatchPropModel) { }; return ( -
-
+
+
- + } /> } /> diff --git a/src/Moryx.CommandCenter.Web/src/index.tsx b/src/Moryx.CommandCenter.Web/src/index.tsx index 3140a2e4c..967607e04 100644 --- a/src/Moryx.CommandCenter.Web/src/index.tsx +++ b/src/Moryx.CommandCenter.Web/src/index.tsx @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0 */ +import { createTheme, ThemeProvider } from "@mui/material/styles"; import * as React from "react"; import * as ReactDOM from "react-dom"; import { Provider } from "react-redux"; @@ -12,12 +13,29 @@ import App from "./common/container/App"; import { AppState, getAppReducer } from "./common/redux/AppState"; import { ActionType } from "./common/redux/Types"; +const theme = createTheme({ + palette: { + primary: { + main: "#24959e", + light: "#c2e1e4", + contrastText: "#fff", + }, + secondary: { + main: "#249e75", + light: "#e1f3ee" + }, + }, + }); + const store = createStore, any, any>(getAppReducer); ReactDOM.render( - + + + + , From 0e84b209915fff7ae4e50a824393066739067f67 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:21:38 +0200 Subject: [PATCH 08/14] Use Material UI for `HealthStateBadge` --- .../dashboard/components/HealthStateBadge.tsx | 6 +++--- .../HealthStateToCssClassConverter.ts | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Moryx.CommandCenter.Web/src/dashboard/components/HealthStateBadge.tsx b/src/Moryx.CommandCenter.Web/src/dashboard/components/HealthStateBadge.tsx index 4bc0c2515..3e6620be5 100644 --- a/src/Moryx.CommandCenter.Web/src/dashboard/components/HealthStateBadge.tsx +++ b/src/Moryx.CommandCenter.Web/src/dashboard/components/HealthStateBadge.tsx @@ -3,8 +3,8 @@ * Licensed under the Apache License, Version 2.0 */ +import Chip from "@mui/material/Chip"; import * as React from "react"; -import { Badge } from "reactstrap"; import { ModuleServerModuleState } from "../../modules/models/ModuleServerModuleState"; import { HealthStateToCssClassConverter } from "../converter/HealthStateToCssClassConverter"; @@ -19,8 +19,8 @@ export class HealthStateBadge extends React.Component { public render(): React.ReactNode { const text = ModuleServerModuleState[this.props.HealthState]; - const color = HealthStateToCssClassConverter.Convert(this.props.HealthState); + const color = HealthStateToCssClassConverter.Color(this.props.HealthState); - return ({text}); + return (); } } diff --git a/src/Moryx.CommandCenter.Web/src/dashboard/converter/HealthStateToCssClassConverter.ts b/src/Moryx.CommandCenter.Web/src/dashboard/converter/HealthStateToCssClassConverter.ts index dcbdca7f9..71921e674 100644 --- a/src/Moryx.CommandCenter.Web/src/dashboard/converter/HealthStateToCssClassConverter.ts +++ b/src/Moryx.CommandCenter.Web/src/dashboard/converter/HealthStateToCssClassConverter.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0 */ +import { ChipPropsColorOverrides } from "@mui/material/Chip"; import * as React from "react"; import { ModuleServerModuleState } from "../../modules/models/ModuleServerModuleState"; @@ -12,31 +13,31 @@ export interface ForegroundBackgroundCssClass { } export class HealthStateToCssClassConverter { - public static Convert(healthState: ModuleServerModuleState): ForegroundBackgroundCssClass { + public static Color(healthState: ModuleServerModuleState): "default" | "primary" | "secondary" | "error" | "info" | "success" | "warning" { switch (healthState) { case ModuleServerModuleState.Failure: { - return { Background: "danger", Foreground: "normal" }; + return "error"; } case ModuleServerModuleState.Initializing: { - return { Background: "info", Foreground: "normal" }; + return "info"; } case ModuleServerModuleState.Ready: { - return { Background: "secondary", Foreground: "composite" }; + return "default"; } case ModuleServerModuleState.Running: { - return { Background: "success", Foreground: "normal" }; + return "success"; } case ModuleServerModuleState.Starting: { - return { Background: "info", Foreground: "normal" }; + return "info"; } case ModuleServerModuleState.Stopping: { - return { Background: "info", Foreground: "normal" }; + return "info"; } case ModuleServerModuleState.Stopped: { - return { Background: "light", Foreground: "composite" }; + return "warning"; } default: { - return { Background: "danger", Foreground: "normal" }; + return "error"; } } } From 26bbf7f166556da1292e28d605f24dfa7ad213db Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:27:07 +0200 Subject: [PATCH 09/14] Use Material UI for module list and module overview This adds Material UI to the module list (navigation) and the module overview. `SectionInfo` and `ModuleInfoTile` got created for code reuse. The Notifications modal got removed in exchange for a collapsible list, which can be browsed like a log file. --- .../src/common/components/ModuleHeader.tsx | 54 +-- .../src/common/components/SectionInfo.tsx | 40 ++ .../src/modules/container/Module.tsx | 366 +++++++----------- .../src/modules/container/ModuleInfoTile.tsx | 41 ++ .../src/modules/container/Modules.tsx | 65 ++-- .../src/modules/container/NotificationRow.tsx | 159 ++++++++ .../src/modules/container/Notifications.tsx | 58 +++ ...duleNotificationTypeToCssClassConverter.ts | 6 +- src/Moryx.CommandCenter.Web/tslint.json | 40 +- 9 files changed, 527 insertions(+), 302 deletions(-) create mode 100644 src/Moryx.CommandCenter.Web/src/common/components/SectionInfo.tsx create mode 100644 src/Moryx.CommandCenter.Web/src/modules/container/ModuleInfoTile.tsx create mode 100644 src/Moryx.CommandCenter.Web/src/modules/container/NotificationRow.tsx create mode 100644 src/Moryx.CommandCenter.Web/src/modules/container/Notifications.tsx diff --git a/src/Moryx.CommandCenter.Web/src/common/components/ModuleHeader.tsx b/src/Moryx.CommandCenter.Web/src/common/components/ModuleHeader.tsx index 8dcb5a21b..ac49cf415 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/ModuleHeader.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/ModuleHeader.tsx @@ -3,28 +3,16 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiCogs, mdiConsole, mdiConsoleLine, mdiDatabase, mdiHexagon, mdiHexagonMultiple, mdiMonitor } from "@mdi/js"; -import Icon from "@mdi/react"; +import Tab from "@mui/material/Tab"; +import Tabs from "@mui/material/Tabs"; import * as React from "react"; import { connect } from "react-redux"; -import { Link, NavLink } from "react-router-dom"; -import { Nav, Navbar, NavItem } from "reactstrap"; +import { Link } from "react-router-dom"; import { FailureBehaviour } from "../../modules/models/FailureBehaviour"; import { ModuleStartBehaviour } from "../../modules/models/ModuleStartBehaviour"; -import ServerModuleModel from "../../modules/models/ServerModuleModel"; import { updateFailureBehaviour, updateStartBehaviour } from "../../modules/redux/ModulesActions"; -import { AppState } from "../redux/AppState"; import { ActionType } from "../redux/Types"; -interface ModuleHeaderPropModel { - Module?: ServerModuleModel; -} - -const mapStateToProps = (state: AppState): ModuleHeaderPropModel => { - return { - }; -}; - const mapDispatchToProps = (dispatch: React.Dispatch>): ModuleDispatchPropModel => { return { onUpdateStartBehaviour: (moduleName: string, startBehaviour: ModuleStartBehaviour) => dispatch(updateStartBehaviour(moduleName, startBehaviour)), @@ -32,41 +20,23 @@ const mapDispatchToProps = (dispatch: React.Dispatch>): ModuleDis }; }; -interface ModulePropModel { +interface ModuleHeaderPropModel { ModuleName: string; + selectedTab: string; } -export class ModuleHeader extends React.Component { - constructor(props: ModulePropModel & ModuleDispatchPropModel) { +export class ModuleHeader extends React.Component { + constructor(props: ModuleHeaderPropModel & ModuleDispatchPropModel) { super(props); - - // This.state = { HasWarningsOrErrors: false, IsNotificationDialogOpened: false, SelectedNotification: null }; } public render(): React.ReactNode { return ( - - - + + + + + ); } } diff --git a/src/Moryx.CommandCenter.Web/src/common/components/SectionInfo.tsx b/src/Moryx.CommandCenter.Web/src/common/components/SectionInfo.tsx new file mode 100644 index 000000000..509c0a67c --- /dev/null +++ b/src/Moryx.CommandCenter.Web/src/common/components/SectionInfo.tsx @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG + * Licensed under the Apache License, Version 2.0 +*/ + +import Grid from "@mui/material/Grid"; +import SvgIcon from "@mui/material/SvgIcon"; +import Typography from "@mui/material/Typography"; +import * as React from "react"; + +interface SectionInfoPropModel { + description: string; + icon: string; +} + +export class SectionInfo extends React.Component { + constructor(props: SectionInfoPropModel) { + super(props); + } + + public render(): React.ReactNode { + return ( + + + + + + + + {this.props.description} + + + ); + } +} diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/Module.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/Module.tsx index d862afc25..08542fe82 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/container/Module.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/container/Module.tsx @@ -3,25 +3,36 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiCheck, mdiDatabase, mdiHexagon, mdiHexagonMultiple, mdiPlay, mdiRestart, mdiStop } from "@mdi/js"; -import Icon from "@mdi/react"; +import { mdiCheck, mdiPlay, mdiRestart, mdiStop } from "@mdi/js"; +import Button from "@mui/material/Button"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Grid from "@mui/material/Grid"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import MenuItem from "@mui/material/MenuItem"; +import SvgIcon from "@mui/material/SvgIcon"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; import * as React from "react"; import { connect } from "react-redux"; -import { Link } from "react-router-dom"; +import { NavLink } from "react-router-dom"; import { toast } from "react-toastify"; -import { Button, ButtonGroup, Card, CardBody, CardHeader, Col, Container, Input, ListGroup, ListGroupItem, Modal, ModalBody, ModalFooter, ModalHeader, Nav, Navbar, NavItem, Row, Table } from "reactstrap"; import ModuleHeader from "../../common/components/ModuleHeader"; import { ActionType } from "../../common/redux/Types"; import { HealthStateBadge } from "../../dashboard/components/HealthStateBadge"; import ModulesRestClient from "../api/ModulesRestClient"; -import { ModuleNotificationTypeToCssClassConverter } from "../converter/ModuleNotificationTypeToCssClassConverter"; import { FailureBehaviour } from "../models/FailureBehaviour"; import { ModuleStartBehaviour } from "../models/ModuleStartBehaviour"; import NotificationModel from "../models/NotificationModel"; -import SerializableException from "../models/SerializableException"; import ServerModuleModel from "../models/ServerModuleModel"; import { Serverity } from "../models/Severity"; import { updateFailureBehaviour, updateStartBehaviour } from "../redux/ModulesActions"; +import { ModuleInfoTile } from "./ModuleInfoTile"; +import { Notifications } from "./Notifications"; interface ModulePropModel { RestClient?: ModulesRestClient; @@ -47,6 +58,7 @@ const mapDispatchToProps = (dispatch: React.Dispatch>): ModuleDis }; class Module extends React.Component { + constructor(props: ModulePropModel & ModuleDispatchPropModel) { super(props); @@ -76,235 +88,155 @@ class Module extends React.Component): void { - const newValue = (e.target as HTMLSelectElement).value as ModuleStartBehaviour; + public onStartBehaviourChange(e: React.ChangeEvent): void { + const newValue = e.target.value as ModuleStartBehaviour; this.props.RestClient.updateModule({ ...this.props.Module, startBehaviour: newValue }).then((d) => this.props.onUpdateStartBehaviour(this.props.Module.name, newValue)); } - public onFailureBehaviourChange(e: React.FormEvent): void { - const newValue = (e.target as HTMLSelectElement).value as FailureBehaviour; + public onFailureBehaviourChange(e: React.ChangeEvent): void { + const newValue = e.target.value as FailureBehaviour; this.props.RestClient.updateModule({ ...this.props.Module, failureBehaviour: newValue }).then((d) => this.props.onUpdateFailureBehaviour(this.props.Module.name, newValue)); } - private openNotificationDetailsDialog(e: React.MouseEvent, notification: NotificationModel): void { - if (notification.exception != null) { - this.setState({ IsNotificationDialogOpened: true, SelectedNotification: notification }); - } - } - - private closeNotificationDetailsDialog(): void { - this.setState({ IsNotificationDialogOpened: false, SelectedNotification: null }); - } - - private static preRenderInnerException(exception: SerializableException): React.ReactNode { - return ( -
- - - Type - - {exception.exceptionTypeName} - - - - Message - {exception.message} - - - {exception.innerException != null && - Module.preRenderInnerException(exception.innerException) - } -
+ private static dependenciesList(module: ServerModuleModel): React.ReactNode { + return ( + {module.dependencies.map((module, idx) => { + return [ + } + disablePadding={true} + component={NavLink} to={"/modules/" + module.name} sx={{color: "black"}} + key={idx} + > + + + + + ]; + })} + ); } public render(): React.ReactNode { + const svgIcon = (path: string) => { + return ( + + + + ); + }; + return ( - - - {this.props.Module.name} - - - - - - - - - - -

Control

- - - - + + + + + + + + + - - -

Error Handling

+
+
+ + {this.state.HasWarningsOrErrors ? ( - + ) : ( - No warnings or errors. + No warnings or errors. )} - -
- - -

General Information

- - - Name: - {this.props.Module.name} - - - State: - - - - Assembly: - {this.props.Module.assembly.name} - - - - -

Dependencies

- {this.props.Module.dependencies.length === 0 ? ( - This module has no dependencies. + + + + + + + } /> + + + + + + + + + + + + + + { + this.props.Module.dependencies.length === 0 ? ( + This module has no dependencies. ) : ( - - - - - - - - - { - this.props.Module.dependencies.map((module, idx) => - - - - ) - } - -
Module NameState
{module.name}
- )} - -
- - -

Start & Failure behaviour

- - - Start behaviour: - - ) => this.onStartBehaviourChange(e)}> - - - - - - - - Failure behaviour: - - ) => this.onFailureBehaviourChange(e)}> - - - - - - - -
- - -

Notifications

- {this.props.Module.notifications.length === 0 ? ( - No notifications detected. - ) : ( - - - - - - - - - - { - this.props.Module.notifications.map((notification, idx) => - ) => this.openNotificationDetailsDialog(e, notification)}> - - - - , - ) - } - -
TypeMessageLevel
{notification.exception != null ? notification.exception.exceptionTypeName : "-"}{notification.message} - - {Serverity[notification.severity]} - -
- )} - -
-
-
- - Notification details - - {this.state.SelectedNotification != null && - - - - - {this.state.SelectedNotification.message} - - - - Type - - - {this.state.SelectedNotification.exception.exceptionTypeName} - - - - - Message - {this.state.SelectedNotification.exception.message} - - - Stack trace - {this.state.SelectedNotification.exception.stackTrace} - - - - {this.state.SelectedNotification.exception.innerException == null ? ( - No inner exception found. - ) : ( - Inner exception - )} - - - {this.state.SelectedNotification.exception.innerException != null && - - {Module.preRenderInnerException(this.state.SelectedNotification.exception.innerException)} - + Module.dependenciesList(this.props.Module) + ) } - - } - - - - - + + + + + ) => this.onStartBehaviourChange(e)} + size="small" + margin="dense" + fullWidth={true}> + Auto + Manual + On dependency + + + + + ) => this.onFailureBehaviourChange(e)} + size="small" + margin="dense" + fullWidth={true} + > + Stop + Stop and notify + + + + + + + + +
); } diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/ModuleInfoTile.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/ModuleInfoTile.tsx new file mode 100644 index 000000000..d9d935f60 --- /dev/null +++ b/src/Moryx.CommandCenter.Web/src/modules/container/ModuleInfoTile.tsx @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG + * Licensed under the Apache License, Version 2.0 +*/ + +import Grid from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; +import * as React from "react"; + +interface ModuleInfoPropModel { + title: string; + spacing?: number; + md?: number; +} + +export class ModuleInfoTile extends React.Component> { + + constructor(props: ModuleInfoPropModel) { + super(props); + + } + + public render() { + return ( + + + {this.props.title} + + + + {this.props.children} + + + ); + } +} diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/Modules.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/Modules.tsx index c57d12d89..ef300b91f 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/container/Modules.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/container/Modules.tsx @@ -3,13 +3,17 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiCogs, mdiComment, mdiConsoleLine, mdiDatabase, mdiHexagon, mdiHexagonMultiple } from "@mdi/js"; -import Icon from "@mdi/react"; +import { mdiCogs, mdiConsoleLine, mdiHexagon, mdiHexagonMultiple } from "@mdi/js"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Grid from "@mui/material/Grid"; +import Tab from "@mui/material/Tab"; +import Tabs from "@mui/material/Tabs"; import * as React from "react"; import { connect } from "react-redux"; import { Link, Route, Routes } from "react-router-dom"; -import { Card, CardBody, CardHeader, Col, ListGroup, Nav, Navbar, NavItem, NavLink, Row } from "reactstrap"; import RoutingMenu from "../../common/components/Menu/RoutingMenu"; +import { SectionInfo } from "../../common/components/SectionInfo"; import MenuItemModel from "../../common/models/MenuItemModel"; import MenuModel from "../../common/models/MenuModel"; import { AppState } from "../../common/redux/AppState"; @@ -78,7 +82,7 @@ class Modules extends React.Component), + Content: (), SubMenuItems: [ { @@ -129,48 +133,31 @@ class Modules extends React.Component - - - - - - - - - - + + + + + + + + - - + + - - - Information - - - Watch, configure and maintain all available modules. Please select a module to proceed... - + + + } /> {this.preRenderRoutesList()}
- - + + ); } } diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/NotificationRow.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/NotificationRow.tsx new file mode 100644 index 000000000..0ba31ba6d --- /dev/null +++ b/src/Moryx.CommandCenter.Web/src/modules/container/NotificationRow.tsx @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG + * Licensed under the Apache License, Version 2.0 +*/ + +import { mdiChevronDown, mdiChevronUp } from "@mdi/js"; +import Collapse from "@mui/material/Collapse"; +import Container from "@mui/material/Container"; +import Grid from "@mui/material/Grid"; +import IconButton from "@mui/material/IconButton"; +import SvgIcon from "@mui/material/SvgIcon"; +import TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import Typography from "@mui/material/Typography"; +import * as React from "react"; +import { ModuleNotificationTypeToCssClassConverter } from "../converter/ModuleNotificationTypeToCssClassConverter"; +import NotificationModel from "../models/NotificationModel"; +import SerializableException from "../models/SerializableException"; +import { Serverity } from "../models/Severity"; + +interface NotificationRowPropModel { + message: NotificationModel; +} + +interface NotificationRowStateModel { + open: boolean; +} + +export class NotificationRow extends React.Component { + + constructor(props: NotificationRowPropModel) { + super(props); + this.state = { + open: false, + }; + } + + private static formatTimestamp(timestamp: Date): string { + const d2 = function (n: number, digits = 2): string { + return String(n).padStart(digits, "0"); + }; + return timestamp.getFullYear() + "-" + + d2(timestamp.getMonth()) + "-" + + d2(timestamp.getDate()) + " " + + d2(timestamp.getHours()) + ":" + + d2(timestamp.getMinutes()) + ":" + + d2(timestamp.getSeconds()) + "." + + timestamp.getMilliseconds(); + } + + public render() { + const message = this.props.message; + return ( + [ + td": { borderBottom: "none" }}} + > + + this.setState({open: !this.state.open})}> + + + + + {

{NotificationRow.formatTimestamp(new Date(message.timestamp.toString()))}

} + {

{Serverity[message.severity]}

} +
+ + {message.message} + + + {message.exception?.exceptionTypeName} + +
, + + + + + + + + + {this.props.message.message} + + + + + Type + + + {this.props.message.exception?.exceptionTypeName} + + + + Message + {this.props.message.exception?.message} + + + + Stack trace + + + {this.props.message.exception?.stackTrace} + + + + + {this.props.message.exception?.innerException == null ? ( + No inner exception found + ) : ( + Inner exception + )} + + + { + this.props.message.exception?.innerException != null && + + {NotificationRow.preRenderInnerException(this.props.message.exception?.innerException)} + + } + + + + + + ] + ); + + } + + private static preRenderInnerException(exception: SerializableException): React.ReactNode { + return ( + + + + Type + + + {exception.exceptionTypeName} + + + + Message + {exception.message} + + {exception.innerException != null && + NotificationRow.preRenderInnerException(exception.innerException) + } + + ); + } +} diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/Notifications.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/Notifications.tsx new file mode 100644 index 000000000..8ee9c3a8a --- /dev/null +++ b/src/Moryx.CommandCenter.Web/src/modules/container/Notifications.tsx @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG + * Licensed under the Apache License, Version 2.0 +*/ + +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import * as React from "react"; +import NotificationModel from "../models/NotificationModel"; +import { NotificationRow } from "./NotificationRow"; + +interface NotificationsPropModel { + messages: NotificationModel[]; +} + +export class Notifications extends React.Component { + + constructor(props: NotificationsPropModel) { + super(props); + + } + + public render() { + const messages = this.props.messages; + return ( + + + + + + + Message + Type + + + + { (messages.length > 0) + ? (messages.map((message, idx) => ( + + + ))) + : + No notifications + + } + +
+
+ ); + + } +} diff --git a/src/Moryx.CommandCenter.Web/src/modules/converter/ModuleNotificationTypeToCssClassConverter.ts b/src/Moryx.CommandCenter.Web/src/modules/converter/ModuleNotificationTypeToCssClassConverter.ts index 40bf7613f..a214f9530 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/converter/ModuleNotificationTypeToCssClassConverter.ts +++ b/src/Moryx.CommandCenter.Web/src/modules/converter/ModuleNotificationTypeToCssClassConverter.ts @@ -13,13 +13,13 @@ export class ModuleNotificationTypeToCssClassConverter { return { color: "black" }; } case Serverity.Warning: { - return { color: "orange" }; + return { color: "#e5ac02" }; } case Serverity.Error: { - return { color: "red" }; + return { color: "#d32f2f" }; } case Serverity.Fatal: { - return { color: "purple" }; + return { color: "#d32f2f" }; } default: { return { color: "black" }; diff --git a/src/Moryx.CommandCenter.Web/tslint.json b/src/Moryx.CommandCenter.Web/tslint.json index e24e7803a..fa437768b 100644 --- a/src/Moryx.CommandCenter.Web/tslint.json +++ b/src/Moryx.CommandCenter.Web/tslint.json @@ -60,7 +60,45 @@ "no-sparse-arrays": true, "no-string-literal": true, "no-string-throw": true, - "no-submodule-imports": [true, "bootstrap-toggle/css/bootstrap2-toggle.css", "uuid/v1" ], // ToDo: define allowed sub modules + "no-submodule-imports": [true, + "@mui/material/Breadcrumbs", + "@mui/material/Button", + "@mui/material/ButtonGroup", + "@mui/material/Card", + "@mui/material/CardContent", + "@mui/material/CircularProgress", + "@mui/material/ClickAwayListener", + "@mui/material/Collapse", + "@mui/material/Container", + "@mui/material/FormControlLabel", + "@mui/material/Grid", + "@mui/material/IconButton", + "@mui/material/Link", + "@mui/material/List", + "@mui/material/ListItemButton", + "@mui/material/ListItemText", + "@mui/material/ListItem", + "@mui/material/MenuItem", + "@mui/material/MenuList", + "@mui/material/Modal", + "@mui/material/Paper", + "@mui/material/Popper", + "@mui/material/Skeleton", + "@mui/material/Stack", + "@mui/material/styles", + "@mui/material/SvgIcon", + "@mui/material/Switch", + "@mui/material/Tab", + "@mui/material/Table", + "@mui/material/TableBody", + "@mui/material/TableCell", + "@mui/material/TableHead", + "@mui/material/TableRow", + "@mui/material/Tabs", + "@mui/material/TextField", + "@mui/material/Tooltip", + "@mui/material/Typography", + "uuid/v1" ], // ToDo: define allowed sub modules "no-switch-case-fall-through": true, "no-this-assignment": true, "no-unbound-method": false, From fd5c2a0233631f2903e34484b1343c7db025bdba Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:38:59 +0200 Subject: [PATCH 10/14] Use Material UI for module configuration Reactstrap hasn't just been replaced by Material UI components here. It seemed easier to slightly change the UI than adjusting all the styling to preserve the previous design: * The entry editors 'open' and 'expand/collapse' buttens have been replaced by a tree navigation. By removing the whole table, the informational column for item titles and descriptions has moved into the input fields and tooltips. * The 'Save and restart' and 'Save' buttons have been merged into a `DropDownButton` to reduce 'primary' actions Also fixes some problems with input change events. --- .../components/ConfigEditor/BooleanEditor.tsx | 20 ++- .../components/ConfigEditor/ByteEditor.tsx | 19 +- .../components/ConfigEditor/ClassEditor.tsx | 7 +- .../CollapsibleEntryEditorBase.tsx | 4 +- .../ConfigEditor/CollectionEditor.tsx | 165 +++++++++-------- .../components/ConfigEditor/ConfigEditor.tsx | 168 +++++++++--------- .../ConfigEditor/InputEditorBase.tsx | 5 +- .../ConfigEditor/NavigableConfigEditor.tsx | 43 +++-- .../components/ConfigEditor/NumberEditor.tsx | 22 ++- .../ConfigEditor/SelectionEditorBase.tsx | 39 ++-- .../components/ConfigEditor/StringEditor.tsx | 36 +++- .../src/modules/components/DropDownButton.tsx | 111 ++++++++++++ .../modules/container/ModuleConfiguration.tsx | 74 ++++---- 13 files changed, 446 insertions(+), 267 deletions(-) create mode 100644 src/Moryx.CommandCenter.Web/src/modules/components/DropDownButton.tsx diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/BooleanEditor.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/BooleanEditor.tsx index 008a96ed4..ce5962d42 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/BooleanEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/BooleanEditor.tsx @@ -3,11 +3,10 @@ * Licensed under the Apache License, Version 2.0 */ -import "bootstrap5-toggle/css/bootstrap5-toggle.css"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; +import Tooltip from "@mui/material/Tooltip"; import * as React from "react"; -import BootstrapToggle from "react-bootstrap-toggle"; -import { Input } from "reactstrap"; -import Entry from "../../models/Entry"; import InputEditorBase, { InputEditorBasePropModel } from "./InputEditorBase"; export default class ByteEditor extends InputEditorBase { @@ -18,10 +17,15 @@ export default class ByteEditor extends InputEditorBase { public render(): React.ReactNode { return ( - ) => this.onToggle(e)} - height="35px" /> + + + ) => this.onToggle(e)} + + />} label={this.props.Entry.displayName}/> + ); } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ByteEditor.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ByteEditor.tsx index d4c372ff2..925f3be73 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ByteEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ByteEditor.tsx @@ -3,8 +3,10 @@ * Licensed under the Apache License, Version 2.0 */ +import MenuItem from "@mui/material/MenuItem"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; import * as React from "react"; -import { Input } from "reactstrap"; import InputEditorBase, { InputEditorBasePropModel } from "./InputEditorBase"; export default class ByteEditor extends InputEditorBase { @@ -16,18 +18,21 @@ export default class ByteEditor extends InputEditorBase { private static preRenderOptions(): React.ReactNode { const options: React.ReactNode[] = []; for (let idx = 0; idx < 256; ++idx) { - options.push(); + options.push({"0x" + idx.toString(16).toUpperCase()}); } return options; } public render(): React.ReactNode { return ( - ) => this.onValueChange(e, this.props.Entry)}> - {ByteEditor.preRenderOptions()} - + + this.onValueChange(e.target.value, this.props.Entry)} + size="small"> + {ByteEditor.preRenderOptions()} + + ); } } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ClassEditor.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ClassEditor.tsx index ab8e95cdc..2b0f48c2b 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ClassEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ClassEditor.tsx @@ -3,8 +3,9 @@ * Licensed under the Apache License, Version 2.0 */ +import Collapse from "@mui/material/Collapse"; +import Container from "@mui/material/Container"; import * as React from "react"; -import { Button, ButtonGroup, Col, Collapse, Container, DropdownItem, DropdownMenu, DropdownToggle, Row, UncontrolledDropdown } from "reactstrap"; import CollapsibleEntryEditorBase, { CollapsibleEntryEditorBasePropModel } from "./CollapsibleEntryEditorBase"; import ConfigEditor from "./ConfigEditor"; @@ -28,8 +29,8 @@ export default class ClassEditor extends CollapsibleEntryEditorBase - - + + {this.preRenderConfigEditor()} diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollapsibleEntryEditorBase.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollapsibleEntryEditorBase.tsx index 04d76e007..e2fb174ad 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollapsibleEntryEditorBase.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollapsibleEntryEditorBase.tsx @@ -3,8 +3,8 @@ * Licensed under the Apache License, Version 2.0 */ +import Collapse from "@mui/material/Collapse"; import * as React from "react"; -import { Collapse } from "reactstrap"; import Entry from "../../models/Entry"; export interface CollapsibleEntryEditorBasePropModel extends React.PropsWithChildren { @@ -23,7 +23,7 @@ export default class CollapsibleEntryEditorBase extends React.Component - + {this.props.children}
diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollectionEditor.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollectionEditor.tsx index 537fec3c6..afc410382 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollectionEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollectionEditor.tsx @@ -3,10 +3,17 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiChevronDown, mdiChevronUp, mdiFolderOpen, mdiPlus, mdiTrashCanOutline } from "@mdi/js"; -import Icon from "@mdi/react"; +import { mdiChevronDown, mdiChevronRight, mdiPlus, mdiTrashCanOutline } from "@mdi/js"; +import Button from "@mui/material/Button"; +import Collapse from "@mui/material/Collapse"; +import Container from "@mui/material/Container"; +import Grid from "@mui/material/Grid"; +import IconButton from "@mui/material/IconButton"; +import MenuItem from "@mui/material/MenuItem"; +import SvgIcon from "@mui/material/SvgIcon"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; import * as React from "react"; -import { Button, ButtonGroup, Col, Collapse, Container, DropdownItem, DropdownMenu, DropdownToggle, Input, Row } from "reactstrap"; import Entry from "../../models/Entry"; import { EntryValueType } from "../../models/EntryValueType"; import BooleanEditor from "./BooleanEditor"; @@ -43,8 +50,8 @@ export default class CollectionEditor extends CollapsibleEntryEditorBase e === entryName) != undefined; } - public onSelect(e: React.FormEvent): void { - this.setState({ SelectedEntry: e.currentTarget.value }); + public onSelect(e: React.ChangeEvent): void { + this.setState({ SelectedEntry: e.target.value }); } public addEntry(): void { @@ -75,7 +82,7 @@ export default class CollectionEditor extends CollapsibleEntryEditorBase ( - options.push() + options.push({colEntry}) )); return options; } @@ -119,75 +126,87 @@ export default class CollectionEditor extends CollapsibleEntryEditorBase - - - { - this.props.Entry.subEntries.map((entry, idx) => { - if (Entry.isClassOrCollection(entry)) { - return ( -
- - {entry.displayName} - - - - - - - - - + + + { + this.props.Entry.subEntries.map((entry, idx) => { + if (Entry.isClassOrCollection(entry)) { + return ( + + + this.toggleCollapsible(entry.uniqueIdentifier ?? entry.identifier)}> + + + + + + + + this.removeEntry(entry)} + disabled={this.props.Entry.value.isReadOnly || this.props.IsReadOnly} + > + + + + + + {this.preRenderConfigEditor(entry)} + -
- ); - } else { - return ( -
- - {this.preRenderConfigEditor(entry)} - - - - - - -
- ); - } - }) - } - - - ) => this.onSelect(e)}> - {this.preRenderOptions()} - - - - -
-
-
+ + + ); + } else { + return ( + + {this.preRenderConfigEditor(entry)} + + this.removeEntry(entry)} + disabled={this.props.Entry.value.isReadOnly || this.props.IsReadOnly} + > + + + + + ); + } + }) + } + + + ) => this.onSelect(e)} + size="small" + fullWidth={true} + > + {this.preRenderOptions()} + + + + this.addEntry()} + disabled={this.props.Entry.value.isReadOnly || this.props.IsReadOnly} + > + + + + + + ); } } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ConfigEditor.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ConfigEditor.tsx index 23a2a6961..cc923099d 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ConfigEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ConfigEditor.tsx @@ -3,10 +3,15 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiChevronDown, mdiChevronUp, mdiFolderOpen} from "@mdi/js"; -import Icon from "@mdi/react"; +import { mdiChevronDown, mdiChevronRight } from "@mdi/js"; +import Button from "@mui/material/Button"; +import Grid from "@mui/material/Grid"; +import IconButton from "@mui/material/IconButton"; +import MenuItem from "@mui/material/MenuItem"; +import SvgIcon from "@mui/material/SvgIcon"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; import * as React from "react"; -import { Button, ButtonGroup, Card, CardBody, CardHeader, Col, Container, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, Input, Row, Table } from "reactstrap"; import Entry from "../../models/Entry"; import { EntryValueType } from "../../models/EntryValueType"; import BooleanEditor from "./BooleanEditor"; @@ -31,7 +36,6 @@ interface ConfigEditorStateModel { } export default class ConfigEditor extends React.Component { - private static divider: number = 2; constructor(props: ConfigEditorPropModel) { super(props); @@ -41,6 +45,13 @@ export default class ConfigEditor extends React.Component, prevState: Readonly, snapshot?: any): void { + if (this.props.ParentEntry && (prevProps.ParentEntry?.value.current !== this.props.ParentEntry.value.current)) { + this.setState({SelectedEntryType: this.props.ParentEntry.value.current}); + + } + } + public componentWillReceiveProps(nextProps: ConfigEditorPropModel): void { if (this.state.SelectedEntryType === "" && this.props.ParentEntry != null) { this.setState({ SelectedEntryType: this.props.ParentEntry.value.current}); @@ -77,13 +88,14 @@ export default class ConfigEditor extends React.Component): void { - this.setState({SelectedEntryType: e.currentTarget.value}); + private onEntryTypeChange(e: React.ChangeEvent): void { + this.setState({SelectedEntryType: e.target.value}); } private onPatchToSelectedEntryType(): void { let prototype: Entry = null; let entryType: EntryValueType = EntryValueType.Class; + if (this.props.ParentEntry != null) { entryType = this.props.ParentEntry.value.type; } @@ -138,16 +150,20 @@ export default class ConfigEditor extends React.Component - - - +
+ this.toggleCollapsible(entry.uniqueIdentifier ?? entry.identifier)}> + + + + + +
); } } @@ -156,53 +172,38 @@ export default class ConfigEditor extends React.Component + return entries.map((subEntry) => ( -
- - - - - {subEntry.displayName} - - - {subEntry.description} - - - - - { - this.selectPropertyByType(subEntry) - } - - + + + { + this.selectPropertyByType(subEntry) + } + + { subEntry.value.type === EntryValueType.Collection && ( - - - - - + + + ) } { subEntry.value.type === EntryValueType.Class && ( - - - - - + + + ) } -
+ )); } @@ -210,50 +211,49 @@ export default class ConfigEditor extends React.Component - + + - - + + ); } else { entries = this.preRenderEntries(this.props.Entries); } return ( -
+ {entries} { ConfigEditor.isEntryTypeSettable(this.props.ParentEntry) && - - - - ) => this.onEntryTypeChange(e)}> - { - this.props.ParentEntry.value.possible.map((possibleValue, idx) => { - return (); - }) - } - - - - - - - + + + ) => this.onEntryTypeChange(e)}> + { + this.props.ParentEntry.value.possible.map((possibleValue, idx) => { + return ({possibleValue}); + }) + } + + + + } -
+ ); } } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/InputEditorBase.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/InputEditorBase.tsx index 8656cd400..36923882c 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/InputEditorBase.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/InputEditorBase.tsx @@ -4,7 +4,6 @@ */ import * as React from "react"; -import { Button, ButtonGroup, Col, Collapse, Container, DropdownItem, DropdownMenu, DropdownToggle, Row, UncontrolledDropdown } from "reactstrap"; import Entry from "../../models/Entry"; export interface InputEditorBasePropModel { @@ -21,8 +20,8 @@ export default class InputEditorBase extends React.Component, entry: Entry): void { - entry.value.current = e.currentTarget.value; + public onValueChange(e: string, entry: Entry): void { + entry.value.current = e; this.forceUpdate(); } } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NavigableConfigEditor.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NavigableConfigEditor.tsx index f2c6882ff..2bcd6be0f 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NavigableConfigEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NavigableConfigEditor.tsx @@ -3,10 +3,13 @@ * Licensed under the Apache License, Version 2.0 */ +import Breadcrumbs from "@mui/material/Breadcrumbs"; +import Grid from "@mui/material/Grid"; +import Link from "@mui/material/Link"; +import Typography from "@mui/material/Typography"; import queryString from "query-string"; import * as React from "react"; import { Location, useLocation, useNavigate } from "react-router-dom"; -import { Button, ButtonGroup, Col, Container, Row } from "reactstrap"; import Entry from "../../models/Entry"; import ConfigEditor from "./ConfigEditor"; @@ -17,12 +20,6 @@ interface NavigableConfigEditorPropModel { Root: Entry; } -interface NavigableConfigEditorStateModel { - EntryChain: Entry[]; - ParentEntry: Entry; - Entries: Entry[]; -} - function NavigableConfigEditor(props: NavigableConfigEditorPropModel) { const location = useLocation(); const navigate = useNavigate(); @@ -82,26 +79,28 @@ function NavigableConfigEditor(props: NavigableConfigEditorPropModel) { }; const preRenderBreadcrumb = (): React.ReactNode => { - const entryChainButtons = entryChain.map((entry, idx) => ( - - )); + const entryChainButtons = entryChain.map((entry, idx) => { + if (idx === entryChain.length - 1) { + return ({entry.displayName}); + } else { + return ( onClickBreadcrumb(entry)}>{entry.displayName}); + } + }); return ( - - + + onClickBreadcrumb(null)}>Home {entryChainButtons} - + ); }; return ( -
- {preRenderBreadcrumb()} - - - Property - Value - + + + {preRenderBreadcrumb()} + + - -
+ + ); } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NumberEditor.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NumberEditor.tsx index a0adfa734..43aba178e 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NumberEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NumberEditor.tsx @@ -3,8 +3,9 @@ * Licensed under the Apache License, Version 2.0 */ +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; import * as React from "react"; -import { Input } from "reactstrap"; import { InputEditorBasePropModel } from "./InputEditorBase"; import SelectionEditorBase from "./SelectionEditorBase"; @@ -14,12 +15,19 @@ export default class NumberEditor extends SelectionEditorBase { } private preRenderInput(): React.ReactNode { - return () => this.onValueChange(e, this.props.Entry)} - placeholder={"Please enter a value of type: " + this.props.Entry.value.type + " ..."} - disabled={this.props.Entry.value.isReadOnly || this.props.IsReadOnly} - value={this.props.Entry.value.current} - />); + const tooltip = this.props.Entry.description + " - Please enter a value of type: " + this.props.Entry.value.type; + return ( + + this.onValueChange(e.target.value, this.props.Entry)} + disabled={this.props.Entry.value.isReadOnly || this.props.IsReadOnly} + size="small" + margin="dense" + /> + ); } public render(): React.ReactNode { diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/SelectionEditorBase.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/SelectionEditorBase.tsx index 2f1d66af7..e30f880d0 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/SelectionEditorBase.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/SelectionEditorBase.tsx @@ -3,8 +3,10 @@ * Licensed under the Apache License, Version 2.0 */ +import MenuItem from "@mui/material/MenuItem"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; import * as React from "react"; -import { Input } from "reactstrap"; import Entry from "../../models/Entry"; import { InputEditorBasePropModel } from "./InputEditorBase"; @@ -15,34 +17,43 @@ interface SelectionStateModel { export default class SelectionEditorBase extends React.Component { constructor(props: InputEditorBasePropModel) { super(props); - if (this.props.Entry.value.possible != null) { const possibleValues: string[] = [...this.props.Entry.value.possible]; if (possibleValues.find((value: string) => value === this.props.Entry.value.current) === undefined) { possibleValues.unshift(""); } - this.state = { PossibleValues: possibleValues }; } + } - public onValueChange(e: React.FormEvent, entry: Entry): void { - entry.value.current = e.currentTarget.value; + public onValueChange(value: string, entry: Entry): void { + entry.value.current = value; this.setState({ PossibleValues: this.props.Entry.value.possible }); this.forceUpdate(); } public render(): React.ReactNode { return ( - ) => this.onValueChange(e, this.props.Entry)}> - { - this.state.PossibleValues.map((possibleValue, idx) => { - return (); - }) - } - + + this.onValueChange(e.target.value, this.props.Entry)}> + { + (this.state != null) ? + this.state.PossibleValues.map((possibleValue, idx) => { + return ({possibleValue}); + }) + : null + } + + ); } } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/StringEditor.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/StringEditor.tsx index 420f2f1cc..8854a3d56 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/StringEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/StringEditor.tsx @@ -3,8 +3,9 @@ * Licensed under the Apache License, Version 2.0 */ +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; import * as React from "react"; -import { Input } from "reactstrap"; import { InputEditorBasePropModel } from "./InputEditorBase"; import SelectionEditorBase from "./SelectionEditorBase"; @@ -14,19 +15,38 @@ export default class StringEditor extends SelectionEditorBase { } private preRenderInput(): React.ReactNode { - return () => this.onValueChange(e, this.props.Entry)} - placeholder={"Please enter a string ..."} - disabled={this.props.Entry.value.isReadOnly || this.props.IsReadOnly} - value={this.props.Entry.value.current == null ? "" : this.props.Entry.value.current} />); + // LoadError is handled here, which is not the right place and should be + // Changed inf the future. + const isLoadError = this.props.Entry.displayName === "LoadError"; + const currentValue = this.props.Entry.value.current; + + return ( + isLoadError && currentValue == null + ? null + : + this.onValueChange(e.target.value, this.props.Entry)} + label={this.props.Entry.displayName} + aria-label={this.props.Entry.description} + fullWidth={true} + error={isLoadError} + multiline={isLoadError} + rows={4} + disabled={(this.props.Entry.value.isReadOnly || this.props.IsReadOnly) && !isLoadError} + value={currentValue == null ? "" : currentValue} + size="small" + margin="dense"/> + ); } private preRenderPossibleValueList(): React.ReactNode { + this.state = { PossibleValues: this.props.Entry.value.possible }; return super.render(); } public render(): React.ReactNode { - return this.props.Entry.value.possible != null && this.props.Entry.value.possible.length > 0 ? - this.preRenderPossibleValueList() : this.preRenderInput(); + return this.props.Entry.value.possible != null && this.props.Entry.value.possible.length > 0 + ? this.preRenderPossibleValueList() + : this.preRenderInput(); } } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/DropDownButton.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/DropDownButton.tsx new file mode 100644 index 000000000..1d66723c5 --- /dev/null +++ b/src/Moryx.CommandCenter.Web/src/modules/components/DropDownButton.tsx @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG + * Licensed under the Apache License, Version 2.0 +*/ + +import { mdiMenuDown } from "@mdi/js"; +import Button from "@mui/material/Button"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import Grow from "@mui/material/Grow"; +import MenuItem from "@mui/material/MenuItem"; +import MenuList from "@mui/material/MenuList"; +import Paper from "@mui/material/Paper"; +import Popper from "@mui/material/Popper"; +import SvgIcon from "@mui/material/SvgIcon"; +import * as React from "react"; + +type OnClickFunction = () => void; + +export interface ButtonConfig { + Label: string; + onClick: OnClickFunction; +} + +export interface ConsoleMethodResultPropModel { + Buttons: ButtonConfig[]; +} + +function DropDownButton(props: ConsoleMethodResultPropModel) { + const [open, setOpen] = React.useState(false); + const [selectedIndex, setSelectedIndex] = React.useState(0); + const anchorRef = React.useRef(null); + + const onMenuItemClick = (event: React.MouseEvent, index: number) => { + setSelectedIndex(index); + setOpen(false); + }; + + const onClose = (event: Event) => { + if ( + anchorRef.current && + anchorRef.current.contains(event.target as HTMLElement) + ) { + return; + } + + setOpen(false); + }; + + const onToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + return ([ + + + + , + + {({ TransitionProps, placement }) => ( + + + + + {props.Buttons.map((button, index) => ( + onMenuItemClick(event, index)} + > + {button.Label} + + ))} + + + + + )} + + ]); +} + +export default DropDownButton; diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConfiguration.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConfiguration.tsx index 3379dcad7..9fef8f294 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConfiguration.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConfiguration.tsx @@ -3,15 +3,20 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiContentSave, mdiHexagon, mdiSync, mdiUndo } from "@mdi/js"; -import Icon from "@mdi/react"; +import { mdiUndo } from "@mdi/js"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import CircularProgress from "@mui/material/CircularProgress"; +import Grid from "@mui/material/Grid"; +import SvgIcon from "@mui/material/SvgIcon"; import * as React from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; -import { Button, ButtonGroup, Card, CardBody, CardHeader, ListGroup, ListGroupItem } from "reactstrap"; import ModuleHeader from "../../common/components/ModuleHeader"; import ModulesRestClient from "../api/ModulesRestClient"; import NavigableConfigEditor from "../components/ConfigEditor/NavigableConfigEditor"; +import DropDownButton from "../components/DropDownButton"; import Config from "../models/Config"; import { ConfigUpdateMode } from "../models/ConfigUpdateMode"; import Entry from "../models/Entry"; @@ -30,7 +35,6 @@ interface ModuleConfigurationStateModel { function ModuleConfiguration(props: ModuleConfigurationPropModel) { const navigate = useNavigate(); - const location = useLocation(); const config = new Config(); config.module = props.ModuleName; @@ -77,41 +81,39 @@ function ModuleConfiguration(props: ModuleConfigurationPropModel) { .then(() => toast.success("Configuration was reverted", { autoClose: 3000 })); }; + const svgIcon = (path: string) => { + return ( + + + + ); + }; + return ( - - - {props.ModuleName} - - - - - - - + + {moduleConfig.ConfigIsLoading && - Loading config ... + } - - - - - - - - + + + + + + + + + + ); } From e87b735c250e2592a172bbb2749205b74d2812b3 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:39:51 +0200 Subject: [PATCH 11/14] Use Material UI for module console --- .../components/ConsoleMethodConfigurator.tsx | 81 ++++++++-------- .../components/ConsoleMethodResult.tsx | 38 +++----- .../src/modules/container/ModuleConsole.tsx | 97 +++++++++---------- 3 files changed, 102 insertions(+), 114 deletions(-) diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConsoleMethodConfigurator.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConsoleMethodConfigurator.tsx index a2393cc27..b86f6ae33 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConsoleMethodConfigurator.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConsoleMethodConfigurator.tsx @@ -3,9 +3,10 @@ * Licensed under the Apache License, Version 2.0 */ +import Button from "@mui/material/Button"; +import Grid from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; import * as React from "react"; -import { useLocation, useNavigate } from "react-router-dom"; -import { Button, Col, Container, Row } from "reactstrap"; import MethodEntry from "../models/MethodEntry"; import NavigableConfigEditor from "./ConfigEditor/NavigableConfigEditor"; @@ -16,51 +17,49 @@ export interface ConsoleMethodConfiguratorPropModel { } function ConsoleMethodConfigurator(props: ConsoleMethodConfiguratorPropModel) { - const navigate = useNavigate(); - const location = useLocation(); - const invokeSelectedMethod = (): void => { props.onInvokeMethod(props.Method); }; return ( -
+ {props.Method == null ? ( - Please select a method. - ) : ( - - - Name: - {props.Method.displayName} - - - Description: - {props.Method.description} - - - - {props.Method.parameters.subEntries.length === 0 ? ( - This method is parameterless. - ) : ( - - )} - - - - - - - - - )} -
+ + Please select a method. + + ) : [ + + + Name: + {props.Method.displayName} + , + + Description: + {props.Method.description} + , + + + {props.Method.parameters.subEntries.length === 0 ? ( + This method is parameterless. + ) : ( + + )} + + , + + + + + + ]} + ); } diff --git a/src/Moryx.CommandCenter.Web/src/modules/components/ConsoleMethodResult.tsx b/src/Moryx.CommandCenter.Web/src/modules/components/ConsoleMethodResult.tsx index adad6bc77..ff3ac3b3e 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConsoleMethodResult.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConsoleMethodResult.tsx @@ -3,9 +3,10 @@ * Licensed under the Apache License, Version 2.0 */ +import Button from "@mui/material/Button"; +import Container from "@mui/material/Container"; +import Grid from "@mui/material/Grid"; import * as React from "react"; -import { useLocation, useNavigate } from "react-router-dom"; -import { Button, Col, Container, Row } from "reactstrap"; import Entry from "../models/Entry"; import MethodEntry from "../models/MethodEntry"; import NavigableConfigEditor from "./ConfigEditor/NavigableConfigEditor"; @@ -17,9 +18,6 @@ export interface ConsoleMethodResultPropModel { } function ConsoleMethodResult(props: ConsoleMethodResultPropModel) { - const navigate = useNavigate(); - const location = useLocation(); - const resetInvokeResult = (): void => { props.onResetInvokeResult(props.Method.name); }; @@ -29,31 +27,25 @@ function ConsoleMethodResult(props: ConsoleMethodResultPropModel) { {props.InvokeResult == null ? ( There is no result. ) : ( - - - Name: - {props.Method.displayName} - - - Description: - {props.Method.description} - - - + + + Name: + {props.Method.displayName} + Description: + {props.Method.description} + - - - - - - - + + )}
diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConsole.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConsole.tsx index ff9f9db4e..7cae42daa 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConsole.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConsole.tsx @@ -3,12 +3,15 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiConsoleLine, mdiHexagon } from "@mdi/js"; -import Icon from "@mdi/react"; -import { number } from "prop-types"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import CircularProgress from "@mui/material/CircularProgress"; +import Grid from "@mui/material/Grid"; +import List from "@mui/material/List"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; import * as React from "react"; import { connect } from "react-redux"; -import { Button, ButtonGroup, Card, CardBody, CardHeader, Col, Container, ListGroup, ListGroupItem, Row } from "reactstrap"; import ModuleHeader from "../../common/components/ModuleHeader"; import { updateShowWaitDialog } from "../../common/redux/CommonActions"; import { ActionType } from "../../common/redux/Types"; @@ -130,26 +133,29 @@ class ModuleConsole extends React.Component - this.onReset()}> - Reset - - - {this.state.Methods.map((methodEntry, idx) => { - return ( - this.onSelectMethod(methodEntry)} - active={this.state.SelectedMethod === methodEntry}> - {methodEntry.displayName} - - ); - }) - } - - + + + this.onReset()} + divider={true}> + + + + {this.state.Methods.map((methodEntry, idx) => { + return [ + this.onSelectMethod(methodEntry)} + selected={this.state.SelectedMethod === methodEntry} + divider={idx < this.state.Methods.length - 1} + > + + , + ]; + }) + } + + ); } @@ -174,37 +180,28 @@ class ModuleConsole extends React.Component - - - {this.preRenderFunctions()} - - - {view} - - - + + + {this.preRenderFunctions()} + + + {view} + + ); } return ( - - - {this.props.ModuleName} - - - - - - - {this.state.IsLoading ? ( - Loading available methods... - ) : ( - content - )} - - + + + + {this.state.IsLoading ? ( + + ) : ( + content + )} + ); } From b62af33ee565d87e5160f215c1d6d36504c9425e Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:44:11 +0200 Subject: [PATCH 12/14] Use Material UI for database configuration This also fixes an issue with drop downs being stuck. For the migrations and setups, the issues with drop downs have been fixed by removing the drop downs at all and providing a list of available items instead. Every list item has an 'action button' for the item (migration or setup) to be executed. So that there is no state anymore to be cached. Also: * Correct or 'similar' configurator will be preselected, even on a version mismatch * UI updates properly, when switching between database contexts * If the database name is `null`, the context name will be used/prefilled as the default --- .../common/components/Menu/RoutingMenu.tsx | 1 - .../components/Menu/RoutingMenuItem.tsx | 2 +- .../src/common/models/MenuItemModel.ts | 2 +- .../src/databases/container/DatabaseModel.tsx | 554 +++++++++--------- .../databases/container/DatabaseSection.tsx | 38 ++ .../src/databases/container/Databases.tsx | 100 ++-- .../src/databases/container/ExecuterList.tsx | 64 ++ 7 files changed, 442 insertions(+), 319 deletions(-) create mode 100644 src/Moryx.CommandCenter.Web/src/databases/container/DatabaseSection.tsx create mode 100644 src/Moryx.CommandCenter.Web/src/databases/container/ExecuterList.tsx diff --git a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx index edfedd664..8b30a825f 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx @@ -26,7 +26,6 @@ function RoutingMenu(props: MenuProps) { return ( {props.MenuItem.Content} diff --git a/src/Moryx.CommandCenter.Web/src/common/models/MenuItemModel.ts b/src/Moryx.CommandCenter.Web/src/common/models/MenuItemModel.ts index ffeaf97b2..942c3648b 100644 --- a/src/Moryx.CommandCenter.Web/src/common/models/MenuItemModel.ts +++ b/src/Moryx.CommandCenter.Web/src/common/models/MenuItemModel.ts @@ -11,7 +11,7 @@ export const enum IconType { } export default interface MenuItemModel { - Secondary?: string; + SecondaryName?: string; Name: string; NavPath: string; SubMenuItems: MenuItemModel[]; diff --git a/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseModel.tsx b/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseModel.tsx index 154fb01d5..5b71ea6b5 100644 --- a/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseModel.tsx +++ b/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseModel.tsx @@ -3,23 +3,38 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiBriefcase, mdiCheck, mdiDatabase, mdiExclamationThick, mdiLoading, mdiPowerPlug, mdiTable } from "@mdi/js"; -import Icon from "@mdi/react"; +import { mdiArrowUpBoldCircleOutline, mdiCogPlayOutline, mdiConnection, mdiDatabaseAlert, mdiDatabaseCheckOutline, mdiTableAlert } from "@mdi/js"; +import Button from "@mui/material/Button"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import CircularProgress from "@mui/material/CircularProgress"; +import Grid from "@mui/material/Grid"; +import MenuItem from "@mui/material/MenuItem"; +import Stack from "@mui/material/Stack"; +import SvgIcon from "@mui/material/SvgIcon"; +import Tab from "@mui/material/Tab"; +import Tabs from "@mui/material/Tabs"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; import * as moment from "moment"; import * as React from "react"; import { connect } from "react-redux"; import { toast } from "react-toastify"; -import { Button, ButtonGroup, Card, CardBody, CardHeader, Col, Container, Form, Input, Nav, NavItem, NavLink, Row, TabContent, TabPane, UncontrolledTooltip } from "reactstrap"; import kbToString from "../../common/converter/ByteConverter"; import { updateShowWaitDialog } from "../../common/redux/CommonActions"; import { ActionType } from "../../common/redux/Types"; -import "../../common/scss/Theme.scss"; import DatabasesRestClient from "../api/DatabasesRestClient"; import DatabaseConfigModel from "../models/DatabaseConfigModel"; +import DatabaseConfigOptionModel from "../models/DatabaseConfigOptionModel"; import DataModel from "../models/DataModel"; import DbMigrationsModel from "../models/DbMigrationsModel"; +import SetupModel from "../models/SetupModel"; import { TestConnectionResult } from "../models/TestConnectionResult"; import { updateDatabaseConfig } from "../redux/DatabaseActions"; +import { DatabaseSection } from "./DatabaseSection"; +import { ExecuterList } from "./ExecuterList"; interface DatabaseModelPropsModel { RestClient: DatabasesRestClient; @@ -27,10 +42,9 @@ interface DatabaseModelPropsModel { } interface DatabaseModelStateModel { - activeTab: string; + activeTab: number; config: DatabaseConfigModel; - selectedMigration: string; - selectedSetup: number; + targetModel: string; selectedBackup: string; testConnectionPending: boolean; testConnectionResult: TestConnectionResult; @@ -52,73 +66,89 @@ class DatabaseModel extends React.Component): void { - this.setState({ selectedMigration: (e.target as HTMLSelectElement).value }); - } - - public onSelectSetup(e: React.FormEvent): void { - this.setState({ selectedSetup: (e.target as HTMLSelectElement).selectedIndex }); - } - - public getValidationState(entryName: string) { + private getValidationState(entryName: string): React.JSX.ElementAttributesProperty { const result = this.props.DataModel.possibleConfigurators.find((x) => x.configuratorTypename === this.state.config.configuratorTypename) - ?.properties.find((x) => x.name === entryName).required ? - (this.state.config.entries[entryName] ? { valid: true, invalid: false } : { invalid: true, valid: false }) : { valid: true, invalid: false }; + ?.properties.find((x) => x.name === entryName).required + ? (this.state.config.entries[entryName] + ? { error: false } + : { error: true }) + : { error: false }; - return result; + return { props: result }; } - public onConfiguratorTypeChanged(e: React.FormEvent): void { - this.setState({ - config: { - ...this.state.config, configuratorTypename: (e.target as HTMLSelectElement).value, - entries: this.getConfigWithDefaultValue((e.target as HTMLSelectElement).value) - } - }); + public onConfiguratorTypeChanged(e: React.ChangeEvent): void { + const config = { ...this.state.config }; + config.configuratorTypename = e.target.value; + config.entries = this.getConfigWithDefaultValue(e.target.value); + this.setState({ config }); } - public onInputChanged(e: React.FormEvent, entryName: string): void { - this.setState({ - config: { ...this.state.config, entries: { ...this.state.config.entries, [entryName]: (e.target as HTMLSelectElement).value } } - }); + public onInputChanged(e: string, entryName: string): void { + const config = {...this.state.config }; + config.entries[entryName] = e; + this.setState({ config }); } - public onSelectBackup(e: React.FormEvent): void { - this.setState({ selectedBackup: (e.target as HTMLSelectElement).value }); + public onSelectBackup(e: React.ChangeEvent): void { + this.setState({ selectedBackup: e.target.value }); } - public createEntriesInput() { + private createEntriesInput(): React.JSX.Element[] { return Object.keys(this.state.config.entries)?.map((element) => { - return ( - this.onTestConnection()} onChange={(e: React.FormEvent) => this.onInputChanged(e, element)} /> - ); + return ( + this.onTestConnection()} + onChange={(e) => this.onInputChanged(e.target.value, element)} + variant="outlined" + size="small" + margin="dense" + /> + ); }); } - public getConfigEntries() { + private getConfigEntries(): any { const newEntries: any = {}; this.props.DataModel.possibleConfigurators[0].properties.forEach((property) => { newEntries[property.name] = ""; @@ -126,15 +156,21 @@ class DatabaseModel extends React.Component x.configuratorTypename === configurator).properties.forEach((property) => { - newEntries[property.name] = property.default ?? ""; - }); + this.props.DataModel.possibleConfigurators + .find((x) => x.configuratorTypename === configuratorName) + .properties.forEach((property) => { + const alternativeDefault = property.name.toLowerCase() === "database" + ? contextNameWithoutNamespace(this.state.targetModel) + : ""; + newEntries[property.name] = property.default ?? alternativeDefault; + + }); return newEntries; } - public getConfigValue() { + public getConfigValue(): DatabaseConfigModel { return { ...this.props.DataModel.config, entries: this.props.DataModel.config.entries ? @@ -146,7 +182,7 @@ class DatabaseModel extends React.Component { + this.props.RestClient.saveDatabaseConfig(this.createConfigModel(), this.state.targetModel).then((response) => { this.props.onShowWaitDialog(false); this.setState({ config: response.config }); @@ -174,7 +210,7 @@ class DatabaseModel extends React.Component this.props.onUpdateDatabaseConfig(databaseConfig)); + this.props.RestClient.databaseModel(this.state.targetModel).then((databaseConfig) => this.props.onUpdateDatabaseConfig(databaseConfig)); toast.success("Database created successfully", { autoClose: 5000 }); } else { toast.error("Database not created: " + data.errorMessage, { autoClose: 5000 }); @@ -184,7 +220,7 @@ class DatabaseModel extends React.Component { + this.props.RestClient.restoreDatabase({ Config: this.createConfigModel(), BackupFileName: this.props.DataModel.targetModel }, this.state.targetModel).then((data) => { this.props.onShowWaitDialog(false); if (data.success) { toast.success("Database restore started successfully. Please refer to the log to get information about the progress.", { autoClose: 5000 }); @@ -229,10 +265,10 @@ class DatabaseModel extends React.Component this.props.onShowWaitDialog(false)); } - public onApplyMigration(): void { + public onApplyMigration(migration: string): void { this.props.onShowWaitDialog(true); - this.props.RestClient.applyMigration(this.props.DataModel.targetModel, this.state.selectedMigration, this.createConfigModel()).then((data) => { + this.props.RestClient.applyMigration(this.props.DataModel.targetModel, migration, this.createConfigModel()).then((data) => { this.props.onShowWaitDialog(false); if (data.wasUpdated) { @@ -259,16 +295,14 @@ class DatabaseModel extends React.Component this.props.onShowWaitDialog(false)); } - public onExecuteSetup(): void { + public onExecuteSetup(setup: SetupModel): void { this.props.onShowWaitDialog(true); - const foundSetup = this.props.DataModel.setups[this.state.selectedSetup]; - - this.props.RestClient.executeSetup(this.props.DataModel.targetModel, { Config: this.createConfigModel(), Setup: foundSetup }).then((data) => { + this.props.RestClient.executeSetup(this.props.DataModel.targetModel, { Config: this.createConfigModel(), Setup: setup }).then((data) => { this.props.onShowWaitDialog(false); if (data.success) { - toast.success("Setup '" + foundSetup.name + "' executed successfully", { autoClose: 5000 }); + toast.success("Setup '" + setup.name + "' executed successfully", { autoClose: 5000 }); } else { toast.error(data.errorMessage, { autoClose: 5000 }); } @@ -278,232 +312,208 @@ class DatabaseModel extends React.Component); + return ( + + ); case TestConnectionResult.ConfigurationError: - return (
- - - - Please check if model configuration exists on server. - -
); + return ( + + + + ); case TestConnectionResult.ConnectionError: - return (
- - - - Please check Database name and connection string. - -
); + return ( + + + + ); case TestConnectionResult.ConnectionOkDbDoesNotExist: - return (
- - - - The connection to the database could be established but the database could not be found. Please check the name of the database or create it before. - -
); + return ( + + + + ); default: return (
); } } + private getMigrations(): DbMigrationsModel[] { + return this.props.DataModel.availableMigrations; + } + public render(): React.ReactNode { return ( - - - {this.props.DataModel.targetModel} - - - - - -

- Connection Settings - {this.state.testConnectionPending ? ( - - ) : this.preRenderConnectionCheckIcon()} -

- -
- - - - - - ) => this.onConfiguratorTypeChanged(e)} - value={this.state.config.configuratorTypename} onBlur={() => this.onTestConnection()}> - {this.props.DataModel.possibleConfigurators.map((config, idx) => ())} - - - {this.state.config.configuratorTypename && this.createEntriesInput()} - - - - - - - - - - - - - ) => this.onSelectBackup(e)}> - { - this.props.DataModel.backups.map((backup, idx) => { - return (); + + + + {contextNameWithoutNamespace(this.state.targetModel)} {this.state.testConnectionPending + ? () + : this.preRenderConnectionCheckIcon()} + + )} + > + + + ) => this.onConfiguratorTypeChanged(e)} + value={reviseConfiguratorName(this.state.config.configuratorTypename, this.props.DataModel.possibleConfigurators)} + onBlur={() => this.onTestConnection()} + variant="outlined" + select={true} + size="small"> + {this.props.DataModel.possibleConfigurators.map((config, idx) => ( + {config.name})) + } + + {this.state.config.configuratorTypename && this.createEntriesInput()} + + + + + + + + + + ) => this.onSelectBackup(e)}> + { + this.props.DataModel.backups.map((backup, idx) => { + return ({backup.fileName + " (Size: " + kbToString(backup.size * 1024) + ", Created on: " + moment(backup.creationDate).format("YYYY-MM-DD HH:mm:ss") + ")"}); + }) + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
); } } +function contextNameWithoutNamespace(databaseModel: string): string { + return databaseModel.replace(/^.+\./, ""); +} + +function reviseConfiguratorName(name: string, possibleConfigurators: DatabaseConfigOptionModel[]): string { + const result = name; + + let configurator = possibleConfigurators.find((pc) => pc.configuratorTypename === result); + + if (configurator != null) { + return result; + } + + configurator = possibleConfigurators.find((pc) => pc.configuratorTypename.startsWith(result.replace(/,.+/, ""))); + + return configurator != null + ? configurator.configuratorTypename + : ""; +} + export default connect<{}, DatabaseModelDispatchPropsModel>(null, mapDispatchToProps)(DatabaseModel); diff --git a/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseSection.tsx b/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseSection.tsx new file mode 100644 index 000000000..755e02e8f --- /dev/null +++ b/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseSection.tsx @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG + * Licensed under the Apache License, Version 2.0 +*/ + +import Grid from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; +import * as React from "react"; + +interface DatabaseSectionPropsModel { + title: React.ReactNode | string; + width?: number; +} + +export class DatabaseSection extends React.Component> { + + constructor(props: React.PropsWithChildren) { + super(props); + } + + public render(): React.ReactNode { + const width = this.props.width ?? 12; + return ( + + + {(typeof this.props.title === "string") + ? {this.props.title} + : this.props.title + } + + + + {this.props.children} + + + ); + } +} diff --git a/src/Moryx.CommandCenter.Web/src/databases/container/Databases.tsx b/src/Moryx.CommandCenter.Web/src/databases/container/Databases.tsx index 6a8bfd09e..c02c3dd23 100644 --- a/src/Moryx.CommandCenter.Web/src/databases/container/Databases.tsx +++ b/src/Moryx.CommandCenter.Web/src/databases/container/Databases.tsx @@ -3,13 +3,21 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiBriefcase, mdiComment, mdiDatabase, mdiHexagonMultiple } from "@mdi/js"; -import Icon from "@mdi/react"; +import { mdiBriefcase, mdiComment, mdiDatabase } from "@mdi/js"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Grid from "@mui/material/Grid"; +import List from "@mui/material/List"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import Skeleton from "@mui/material/Skeleton"; +import Tab from "@mui/material/Tab"; +import Tabs from "@mui/material/Tabs"; import * as React from "react"; import { connect } from "react-redux"; import { Link, Route, Routes } from "react-router-dom"; -import { Card, CardBody, CardHeader, Col, ListGroup, Nav, Navbar, NavItem, Row } from "reactstrap"; import RoutingMenu from "../../common/components/Menu/RoutingMenu"; +import { SectionInfo } from "../../common/components/SectionInfo"; import MenuItemModel from "../../common/models/MenuItemModel"; import MenuModel from "../../common/models/MenuModel"; import { AppState } from "../../common/redux/AppState"; @@ -73,7 +81,7 @@ class Database extends React.Component{namespace}

), + SecondaryName: namespace, SubMenuItems: [], }; } @@ -94,52 +102,56 @@ class Database extends React.Component - - - - - - - - - {this.state.IsLoading ? ( - Loading... - ) : ( - - )} - + + + + + + + + {this.state.IsLoading ? ( + + + + + + + + + + + + + + + ) : ( + + )} - - + + - - - Information - - - Configure all available database models. Please select a database model to proceed... - - } /> + + + + } + /> {this.preRenderRoutesList()} - - + + ); } } diff --git a/src/Moryx.CommandCenter.Web/src/databases/container/ExecuterList.tsx b/src/Moryx.CommandCenter.Web/src/databases/container/ExecuterList.tsx new file mode 100644 index 000000000..7314bad80 --- /dev/null +++ b/src/Moryx.CommandCenter.Web/src/databases/container/ExecuterList.tsx @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG + * Licensed under the Apache License, Version 2.0 +*/ + +import IconButton from "@mui/material/IconButton"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemText from "@mui/material/ListItemText"; +import SvgIcon from "@mui/material/SvgIcon"; +import * as React from "react"; + +type OnExecuteFunction = () => void; + +export interface ExecuterItem { + title: string; + subtitle: string; + icon: string; + enabled: boolean; + hidden: boolean; + onExecute: OnExecuteFunction; +} + +interface ExecuterListPropsModel { + items: ExecuterItem[]; +} + +export class ExecuterList extends React.Component { + + constructor(props: ExecuterListPropsModel) { + super(props); + } + + public render(): React.ReactNode { + const items = this.props.items; + return ( + + { + items.map((item, idx) => { + return ( + + ) + }> + + ); + }) + } + + ); + } +} From 28855461a778fd3a28395b23f0dd70ad7d89e168 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Thu, 25 Apr 2024 13:01:11 +0200 Subject: [PATCH 13/14] Bring NavLink to RoutingMenu --- .../common/components/Menu/RoutingMenu.tsx | 11 ----- .../components/Menu/RoutingMenuItem.tsx | 47 +++++++------------ 2 files changed, 18 insertions(+), 40 deletions(-) diff --git a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx index 8b30a825f..aaddc1cf1 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenu.tsx @@ -5,21 +5,11 @@ import List from "@mui/material/List"; import * as React from "react"; -import { useNavigate } from "react-router-dom"; import MenuItemModel from "../../models/MenuItemModel"; import { MenuProps } from "../../models/MenuModel"; import RoutingMenuItem from "./RoutingMenuItem"; function RoutingMenu(props: MenuProps) { - const navigate = useNavigate(); - - const handleMenuItemClick = (menuItem: MenuItemModel): void => { - if (props.onActiveMenuItemChanged != null) { - props.onActiveMenuItemChanged(menuItem); - } - navigate(menuItem.NavPath); - }; - const renderMenu = (menuItems: MenuItemModel[]): React.ReactNode => { return { menuItems.map((menuItem, idx) => { @@ -29,7 +19,6 @@ function RoutingMenu(props: MenuProps) { MenuItem={menuItem} Level={0} Divider={idx < menuItems.length - 1} - onMenuItemClicked={(menuItem) => handleMenuItemClick(menuItem)} /> ); }) diff --git a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx index 0ae24ed1d..23682b5ae 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx @@ -7,7 +7,7 @@ import ListItem from "@mui/material/ListItem"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemText from "@mui/material/ListItemText"; import * as React from "react"; -import { Location, useLocation, useNavigate } from "react-router-dom"; +import { Location, NavLink, useLocation } from "react-router-dom"; import MenuItemModel from "../../models/MenuItemModel"; interface MenuItemProps { @@ -15,26 +15,10 @@ interface MenuItemProps { MenuItem: MenuItemModel; Level: number; Divider: boolean; - onMenuItemClicked?(menuItem: MenuItemModel): void; } function RoutingMenuItem(props: MenuItemProps) { const location = useLocation(); - const navigate = useNavigate(); - - React.useEffect(() => { - }, [navigate]); - - const handleMenuItemClick = (e: React.MouseEvent): void => { - e.preventDefault(); - onMenuItemClicked(props.MenuItem); - }; - - const onMenuItemClicked = (menuItem: MenuItemModel): void => { - if (props.onMenuItemClicked != null) { - props.onMenuItemClicked(menuItem); - } - }; const isActive = (location: Location): boolean => { // Path has to be equal to be 'active' or must be a sub path (following @@ -49,19 +33,24 @@ function RoutingMenuItem(props: MenuItemProps) { const isLocationActive = isActive(location); return ( - - ) => handleMenuItemClick(e)} - divider={props.Divider} + - - {props.MenuItem.Content} - - + + + {props.MenuItem.Content} + + ); } From 2cbb7c8949f2d073969b3b554c42b72541735840 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 22 Apr 2024 15:49:56 +0200 Subject: [PATCH 14/14] Remove reactstrap related packages and all custom scss By switching to Material UI, depending on reactstrap and related packages is not required anymore. Also, all the custom css definitions are obsolete. This stops styles from being overwritten by parent UIs and vice versa. --- src/Moryx.CommandCenter.Web/package.json | 4 - .../src/common/scss/Button.scss | 54 -- .../src/common/scss/Form.scss | 35 -- .../src/common/scss/Header.scss | 21 - .../src/common/scss/Menu.scss | 33 -- .../src/common/scss/Theme.scss | 37 -- .../src/common/scss/commandcenter.scss | 525 ------------------ 7 files changed, 709 deletions(-) delete mode 100644 src/Moryx.CommandCenter.Web/src/common/scss/Button.scss delete mode 100644 src/Moryx.CommandCenter.Web/src/common/scss/Form.scss delete mode 100644 src/Moryx.CommandCenter.Web/src/common/scss/Header.scss delete mode 100644 src/Moryx.CommandCenter.Web/src/common/scss/Menu.scss delete mode 100644 src/Moryx.CommandCenter.Web/src/common/scss/Theme.scss delete mode 100644 src/Moryx.CommandCenter.Web/src/common/scss/commandcenter.scss diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index 9f7586c74..e5441ec98 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -21,20 +21,16 @@ "@types/react-dom": "^18.2.19", "@types/react-redux": "^7.1.33", "@types/uuid": "^9.0.8", - "bootstrap": "5.3.3", - "bootstrap5-toggle": "^5.0.6", "moment": "^2.30.1", "path-scurry": "^1.10.2", "query-string": "^9.0.0", "react": "18.2.0", - "react-bootstrap-toggle": "^2.3.2", "react-dom": "18.2.0", "react-redux": "^9.1.0", "react-router": "^6.22.0", "react-router-dom": "^6.22.0", "react-router-redux": "^4.0.8", "react-toastify": "^10.0.4", - "reactstrap": "^9.2.2", "redux": "^5.0.1", "ts-loader": "^9.5.1", "uuid": "^9.0.1" diff --git a/src/Moryx.CommandCenter.Web/src/common/scss/Button.scss b/src/Moryx.CommandCenter.Web/src/common/scss/Button.scss deleted file mode 100644 index b615350e3..000000000 --- a/src/Moryx.CommandCenter.Web/src/common/scss/Button.scss +++ /dev/null @@ -1,54 +0,0 @@ -@import "./Theme"; - -.btn:hover:enabled, -.btn:focus:enabled, -.btn.focus:enabled { - color: #24959e; - text-decoration: none; -} - -.btn-default { - color: #24959e; - background-color: #ffffff; - border-color: #cccccc; -} -.btn-default:focus:enabled, -.btn-default.focus:enabled { - color: #24959e; - background-color: #e6e6e6; - border-color: #8c8c8c; -} -.btn-default:hover:enabled { - color: #24959e; - background-color: #e6e6e6; - border-color: #adadad; -} -.btn-default:active:enabled, -.btn-default.active:enabled, -.open > .dropdown-toggle.btn-default { - color: #24959e; - background-color: #e6e6e6; - border-color: #adadad; -} - -.btn-default .badge { - color: #ffffff; - background-color: #24959e; -} -.btn-primary { - color: #ffffff; - background-color: #24959e; - border-color: #1f8189; -} -.btn-primary:focus:enabled, -.btn-primary.focus:enabled { - color: #ffffff; - background-color: #1b6e74; - border-color: #081f21; -} -.btn-primary:hover:enabled { - color: #ffffff; - background-color: #1b6e74; - border-color: #145257; -} - diff --git a/src/Moryx.CommandCenter.Web/src/common/scss/Form.scss b/src/Moryx.CommandCenter.Web/src/common/scss/Form.scss deleted file mode 100644 index fd769735f..000000000 --- a/src/Moryx.CommandCenter.Web/src/common/scss/Form.scss +++ /dev/null @@ -1,35 +0,0 @@ -@import "./Theme"; - -.form-control { - height: 1.95rem !important; -} - -.form-control, -.form-select { - border: 1px solid #ced4da; - color: #495057; -} - -.log-menu { - padding: 0px; -} - -.log-select { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border: none; - border-radius: 0; - font-size: 1em; - background-repeat: no-repeat; - background-position: 3px 3px; - background-color: transparent; - width: 20px; - color: transparent; - cursor: pointer; -} - -.log-select option { - color: black; - font-size: .85em; -} \ No newline at end of file diff --git a/src/Moryx.CommandCenter.Web/src/common/scss/Header.scss b/src/Moryx.CommandCenter.Web/src/common/scss/Header.scss deleted file mode 100644 index 60a7e4887..000000000 --- a/src/Moryx.CommandCenter.Web/src/common/scss/Header.scss +++ /dev/null @@ -1,21 +0,0 @@ -@import "./Theme"; - -h1, h2, h3, { - font-weight: bold; -} - -h1 { - font-size: 20px; -} - -h2 { - font-size: 18px; -} - -h3 { - font-size: 16px; -} - -.header { - margin: 10px 10px 0px 10px; -} \ No newline at end of file diff --git a/src/Moryx.CommandCenter.Web/src/common/scss/Menu.scss b/src/Moryx.CommandCenter.Web/src/common/scss/Menu.scss deleted file mode 100644 index cbfd2aeba..000000000 --- a/src/Moryx.CommandCenter.Web/src/common/scss/Menu.scss +++ /dev/null @@ -1,33 +0,0 @@ -@import "Theme"; - -.menu-item { - cursor: pointer; - line-height: 1.15; - font-size: 11pt; - border-bottom: 1px solid rgba(0, 0, 0, 0.125); - border-left: 0px; - border-right: 0px; - border-top: 0px; - padding: .75rem 1.25rem; -} - -.menu-item:hover { - background: $gray-200; -} - -.menu-item a { - color: $black; - text-decoration: none; -} - -.menu-item a:hover { - text-decoration: none; -} - -.menu-item.active { - background: $gray-300; - border-color: $gray-300; - border-left: 0px; - border-right: 0px; - border-radius: 0px; -} \ No newline at end of file diff --git a/src/Moryx.CommandCenter.Web/src/common/scss/Theme.scss b/src/Moryx.CommandCenter.Web/src/common/scss/Theme.scss deleted file mode 100644 index 88bf548e7..000000000 --- a/src/Moryx.CommandCenter.Web/src/common/scss/Theme.scss +++ /dev/null @@ -1,37 +0,0 @@ -%custom-bootstrap-variables { - @import "./node_modules/bootstrap/scss/functions"; - @import "./node_modules/bootstrap/scss/variables"; - - // Variables - // - // Variables should follow the `$component-state-property-size` formula for - // consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs. - - $blue: #007bff !global; - $indigo: #6610f2 !global; - $purple: #6f42c1 !global; - $pink: #e83e8c !global; - $red: #d9534f !global; - $orange: #f0ad4e !global; - $yellow: #ffc107 !global; - $green: #34861c !global; - $teal: #5bc0de !global; - $cyan: #17a2b8 !global; - $phoenix: #24959E !global; - - $primary: $phoenix !global; - $secondary: $white !global; - $success: $green !global; - $info: $cyan !global; - $warning: $yellow !global; - $danger: $red !global; - $light: $gray-300 !global; - $dark: $gray-800 !global; - - $border-radius: 4px !global; - $border-radius-lg: 6px !global; - $border-radius-sm: 3px !global; - $border-color: $gray-800 !global; - - $font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif !global; -} \ No newline at end of file diff --git a/src/Moryx.CommandCenter.Web/src/common/scss/commandcenter.scss b/src/Moryx.CommandCenter.Web/src/common/scss/commandcenter.scss deleted file mode 100644 index a69d9decc..000000000 --- a/src/Moryx.CommandCenter.Web/src/common/scss/commandcenter.scss +++ /dev/null @@ -1,525 +0,0 @@ -@import "Theme"; -@import "node_modules/bootstrap/scss/bootstrap"; -@import "Button"; -@import "Header"; -@import "Form"; -@import "Menu"; - -/* - * General - */ -.selectable { - cursor: pointer; -} - -.selectable.nav-link { - color: inherit; -} - -/* - * Main Body - */ -#pxclogo { - height: 40px; - margin-top: 10px; - background: transparent; -} - -#clock { - margin-top: 10px; - height: 40px; - font-weight: bold; -} - -body { - padding-top: 0px; - background: #ffffff; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #333; -} - -svg.icon { - height: 20px; -} - -svg.icon-white { - height: 20px; - - path { - fill: #ffffff; - } -} - -svg.icon-green { - height: 20px; - - path { - fill: green; - } -} - -svg.icon-red { - height: 20px; - - path { - fill: red; - } -} - -.right-space { - margin-right: 5px; -} - -.up-space { - margin-top: 5px; -} - -.up-space-lg { - margin-top: 10px; -} - -.up-space-very-lg { - margin-top: 20px; -} - -.down-space { - margin-bottom: 5px; -} - -.down-space-lg { - margin-bottom: 10px; -} - -.down-space-very-lg { - margin-bottom: 20px; -} - -.bg-success { - background: $green !important; -} - -.badge-success { - color: #fff; - background-color: $green; -} - -.lg-height { - height: 70px !important; -} - -.center-text { - text-align: -webkit-center; -} - -.no-padding { - padding: 0px; -} - -/* - * Navigation - */ -.card-header { - margin-bottom: 0 !important; - height: 50px; - padding: 0; - display: flex; - align-items: center; - justify-content: center; -} - -.navbar { - min-height: unset !important; - transition: 0.3s; - overflow: hidden; - padding: 0; - height: inherit; -} - -.navbar-default.navbar.navbar-expand-md { - background-color: transparent; -} - -.navbar-left.navbar-nav { - width: 100%; - list-style-type: none; - margin-left: 0; - margin-top: 0; - height: inherit; -} - -.nav-item { - height: 100%; - display: flex; - align-items: center; - margin-top: 0 !important; - justify-content: center; - border-right: 1px solid $gray-200; -} - -.nav-item:last-child { - border: none; -} - -.nav-item:hover { - background: $gray-200; - cursor: pointer; - color: $gray-600; -} - -.active { - background: $gray-300; - margin-top: 0 !important; -} - -.nav-tabs { - border-bottom: 1px solid #dee2e6; - - .nav-link.active { - border-color: #dee2e6 #dee2e6 #fff; - color: #495057; - } - - .nav-link:hover { - border-color: #e9ecef #e9ecef #dee2e6; - } -} - -.table th { - border-top: 1px solid #343a40; -} - -.navbar-nav-link { - color: $gray-800; - text-decoration: none; - height: 100%; - padding: 5px; -} - -a.navbar-nav-link { - height: 100%; - display: flex; - align-items: center; - justify-content: center; - padding: 0px 7px; -} - -.navbar-nav-link:hover, -.navbar-nav-link:focus { - color: $gray-600; - text-decoration: none; -} - -.navbar-collapse { - height: 50px; -} - -.nav-listgroup-item { - padding: 0; - height: 40px; - border: 1px solid rgba(0, 0, 0, 0.125); - border-top: none; - border-left: none; - border-right: none; -} - -.table { - thead th { - border-bottom: 2px solid #343a40; - } - - td { - border-top: 1px solid #343a40; - padding: .75rem; - } -} - -.table> :not(caption)>*>* { - border-bottom: none; -} - -/* - * Content Panel - */ -.commandcenter-app-container { - display: flex; - flex-direction: column; -} - -.commandcenter-content { - margin-top: 20px; - height: 100%; -} - -.component { - border-radius: 4px; -} - -.card { - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); - transition: 0.3s; - overflow: hidden; -} - -/* - * Config Editor - */ -.config-editor-header { - padding: 2px 0px 2px 0px; - margin: 0px; - border-style: solid; - border-width: 1px 0px 2px 0px; - border-color: lightgray; - font-weight: bold; -} - -.config-editor { - margin-top: 10px; - margin-bottom: 10px; - width: 100%; - max-width: 100%; - margin-bottom: 20px; - background-color: transparent; - - .actionButton { - @media (min-width: 768px) { - min-width: 120px; - } - - min-width: auto; - } - - .row { - margin-right: 0px; - margin-left: 0px; - } - - .entry-row { - padding: 5px 0px 5px 0px; - align-items: "center"; - } - - .table-row { - align-items: "center"; - - &:nth-child(even) { - background-color: #f2f2f2; - } - - &:nth-child(odd) { - background-color: white; - } - } - - .name-column { - padding: 10px 0px 10px 10px; - - .property-description { - color: darkgray; - margin: 0 0 0; - } - } - - .property-column { - padding: 10px 0px 10px 10px; - } -} - -.expanded-collection { - .collection-entry { - &:nth-child(even) { - background-color: white; - } - - &:nth-child(odd) { - background-color: #edf0f3; - } - } -} - -.col-name { - padding: 15px 0px 15px 15px; - float: left; -} - -.col-btn { - padding: 15px 15px 15px 0px; - float: right; -} - -/* - * General module view - */ -.btn-group.module-btn-group { - .btn { - min-width: 70px; - - @media(min-width: 375px) { - min-width: 100px; - } - } -} - -.toggle-on { - min-width: 62px; -} - -.card>.list-group { - border-bottom: none; -} - -.list-group { - margin: 0 !important; -} - -.list-group-item { - margin-top: 0 !important; -} - -.list-group-item.selectable { - padding: .75rem 1.25rem; - border: 1px solid rgba(0, 0, 0, 0.125); -} - -.list-group-item.selectable:not(:first-child) { - border-top: none; -} - -/* - * Sidebar - */ -.sidebar-panel { - .panel-body { - padding: 0px; - } - - .nav-wrapper { - position: relative; - min-height: 50px; - } - - .navlist { - padding-left: 0; - margin-bottom: 0; - list-style: none; - } - - .navitem>a, - .nav-subitem { - position: relative; - display: block; - cursor: pointer; - - &:hover, - &.active { - background-color: #eee; - } - - .navdropdown { - padding-left: 20px; - } - } - - .navitem>a, - .nav-subitem>a { - //color: $linkColor; - position: relative; - display: block; - padding: 10px 10px; - text-decoration: none; - } -} - -/* - * Modals - */ -.modal { - text-align: center; - padding: 0 !important; -} - -.modal:before { - content: ''; - display: inline-block; - height: 100%; - vertical-align: middle; - margin-right: -4px; -} - -.modal-dialog { - display: inline-block; - text-align: left; - vertical-align: middle; -} - -.log-modal-dialog { - max-width: 65% !important; -} - -.notification-modal-dialog { - max-width: 90% !important; -} - -/* - * Font - */ -.font-bold { - font-weight: bold; -} - -.font-italic { - font-style: italic; -} - -.font-disabled { - color: gray; -} - -.font-normal { - font-size: 16px; -} - -.font-small { - font-size: 14px; -} - -.font-smaller { - font-size: 12px; -} - -.font-smallest { - font-size: 10px; -} - -/* - * Progress - */ -.progress { - height: 20px; - background: whitesmoke; -} - -.progress-bar { - background: orangered; - color: black; -} - -/* - * Overwrite bootstrap styles that would otherwise - * conflict with MORYX material styles -*/ -a { - text-decoration-line: none; -} - -.active>.container-fluid { - padding: calc(var(--bs-gutter-x) * .5) -} - -.border-bottom { - border-bottom: 1px solid #e5e5e5 !important; -} - -.px-4 { - padding-left: 1rem !important; - padding-right: 1rem !important; -} \ No newline at end of file