From 63f198010ca2855fe2e08856dc97c86d4deef8fd Mon Sep 17 00:00:00 2001 From: MarisaGoergen <69142022+MarisaGoergen@users.noreply.github.com> Date: Wed, 31 May 2023 15:24:08 +0200 Subject: [PATCH 01/82] Fix spelling mistakes in the documentation --- docs/tutorials/HowToCreateAProduct.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/HowToCreateAProduct.md b/docs/tutorials/HowToCreateAProduct.md index c62cb9f8e..2a00fdc6f 100644 --- a/docs/tutorials/HowToCreateAProduct.md +++ b/docs/tutorials/HowToCreateAProduct.md @@ -1,11 +1,11 @@ --- uid: HowToCreateAProduct --- -# How to create a resource +# How to create a product This tutorial shows how [Products](../../src/Moryx.AbstractionLayer/Products/ProductType.cs) should be implemented. Look [here](../articles/Products/Concept.md) if you are not firm with the concept of a `Product`. -For products we differentiate between [ProductType](../../src/Moryx.AbstractionLayer/Products/ProductType.cs) and [ProductInstance](../../src/Moryx.AbstractionLayer/Products/ProductInstance.cs). The `ProductType` is what you can order in a catalog, while the `ProductInstance` is what you received after ordering: an instance of the product with its unique serialnumber. So that a `ProductType` can be producted it needs a corresponding `ProductInstance`. If your application isn't used for production, you can skip the `ProductInstances`. +For products we differentiate between [ProductType](../../src/Moryx.AbstractionLayer/Products/ProductType.cs) and [ProductInstance](../../src/Moryx.AbstractionLayer/Products/ProductInstance.cs). The `ProductType` is what you can order in a catalog, while the `ProductInstance` is what you received after ordering: an instance of the product with its unique serialnumber. So that a `ProductType` can be produced it needs a corresponding `ProductInstance`. If your application isn't used for production, you can skip the `ProductInstances`. ## Create a basic ProductType and ProductInstance All ProductTypes are derived from `ProductType`. The methode `Instantiate()` returns an object of the correspoding `ProductInstance`. From 684698a76a0afc619ea05dd9db9f9be0e81107d0 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus <37485711+1nf0rmagician@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:32:09 +0100 Subject: [PATCH 02/82] Update used tools branch for github workflow --- .github/workflows/build-and-test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f276f12f5..a6f81c1c2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -23,7 +23,7 @@ env: jobs: Build: - uses: PHOENIXCONTACT/tools/.github/workflows/build-tool.yml@main + uses: PHOENIXCONTACT/tools/.github/workflows/build-tool.yml@release-6 with: MORYX_OPTIMIZE_CODE: "false" MORYX_BUILD_CONFIG: "Release" @@ -34,7 +34,7 @@ jobs: UnitTests: needs: [Build] - uses: PHOENIXCONTACT/tools/.github/workflows/unittest-tool.yml@main + uses: PHOENIXCONTACT/tools/.github/workflows/unittest-tool.yml@release-6 with: MORYX_OPTIMIZE_CODE: "false" MORYX_BUILD_CONFIG: "Release" @@ -45,7 +45,7 @@ jobs: IntegrationTests: needs: [Build] - uses: PHOENIXCONTACT/tools/.github/workflows/integrationtest-tool.yml@main + uses: PHOENIXCONTACT/tools/.github/workflows/integrationtest-tool.yml@release-6 with: MORYX_OPTIMIZE_CODE: "false" MORYX_BUILD_CONFIG: "Release" @@ -56,7 +56,7 @@ jobs: ReportGenerator: needs: [UnitTests, IntegrationTests] - uses: PHOENIXCONTACT/tools/.github/workflows/reportgenerator-tool.yml@main + uses: PHOENIXCONTACT/tools/.github/workflows/reportgenerator-tool.yml@release-6 with: MORYX_OPTIMIZE_CODE: "false" MORYX_BUILD_CONFIG: "Release" @@ -67,7 +67,7 @@ jobs: Publish-Test-Coverage: needs: [ReportGenerator] - uses: PHOENIXCONTACT/tools/.github/workflows/publish-test-coverage-tool.yml@main + uses: PHOENIXCONTACT/tools/.github/workflows/publish-test-coverage-tool.yml@release-6 with: MORYX_OPTIMIZE_CODE: "false" MORYX_BUILD_CONFIG: "Release" @@ -81,7 +81,7 @@ jobs: Documentation: needs: [UnitTests] - uses: PHOENIXCONTACT/tools/.github/workflows/documentation-tool.yml@main + uses: PHOENIXCONTACT/tools/.github/workflows/documentation-tool.yml@release-6 with: MORYX_OPTIMIZE_CODE: "false" MORYX_BUILD_CONFIG: "Release" @@ -92,7 +92,7 @@ jobs: Publish: needs: [UnitTests] - uses: PHOENIXCONTACT/tools/.github/workflows/publish-tool.yml@main + uses: PHOENIXCONTACT/tools/.github/workflows/publish-tool.yml@release-6 with: MORYX_OPTIMIZE_CODE: "false" MORYX_BUILD_CONFIG: "Release" From aedd751d03052f40c0af92fd1fb57d4d2487de82 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Thu, 23 Nov 2023 06:17:10 +0100 Subject: [PATCH 03/82] Bump version to 8.0.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ae9a76b92..cd1d2e94f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.0 +8.0.1 From 6ac67853262a7f4f949469bf183e7ae1847a77e4 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Tue, 28 Nov 2023 06:20:36 +0100 Subject: [PATCH 04/82] Update migration guide --- docs/migrations/v6_to_v8.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/migrations/v6_to_v8.md b/docs/migrations/v6_to_v8.md index 349237ca9..7212ddae1 100644 --- a/docs/migrations/v6_to_v8.md +++ b/docs/migrations/v6_to_v8.md @@ -22,4 +22,8 @@ Removed all overrides of the obsolete method `Exception.GetObjectData(Serializat The following classes are affected by this change - MissingFacadeException - HealthStateException -- InvalidConfigException \ No newline at end of file +- InvalidConfigException + +## Merged IPublicResource into IResource + +`IPublicResource` and `IResource` were merged into `IResource`, since the differentiaten between those was hard to understand for some and barely had any real world advantages. Now literally "Everything is a resource". \ No newline at end of file From a4d333d0cbef877ffa2cc26e5c0d0f0aa796968b Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Tue, 23 Jan 2024 11:10:45 +0100 Subject: [PATCH 05/82] Fix GetInstance(IIdentity) resulting in an exception because of a wrong if clause --- .../Products/IProductManagement.cs | 2 ++ .../Facades/ProductManagementFacade.cs | 18 +++++++++++++----- .../Implementation/Storage/ProductStorage.cs | 18 ++++++++++++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Moryx.AbstractionLayer/Products/IProductManagement.cs b/src/Moryx.AbstractionLayer/Products/IProductManagement.cs index bfa9072fb..c5dae83ff 100644 --- a/src/Moryx.AbstractionLayer/Products/IProductManagement.cs +++ b/src/Moryx.AbstractionLayer/Products/IProductManagement.cs @@ -94,6 +94,8 @@ public interface IProductManagement : IRecipeProvider, IWorkplans /// /// Get an instance with this identity /// + /// Thrown when is null + /// Thrown when there is more than one product with the given ProductInstance GetInstance(IIdentity identity); /// diff --git a/src/Moryx.Products.Management/Facades/ProductManagementFacade.cs b/src/Moryx.Products.Management/Facades/ProductManagementFacade.cs index 9a12528b6..aa66686d1 100644 --- a/src/Moryx.Products.Management/Facades/ProductManagementFacade.cs +++ b/src/Moryx.Products.Management/Facades/ProductManagementFacade.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Copyright (c) 2024, Phoenix Contact GmbH & Co. KG // Licensed under the Apache License, Version 2.0 using System; @@ -9,6 +9,7 @@ using Moryx.AbstractionLayer.Identity; using Moryx.AbstractionLayer.Products; using Moryx.AbstractionLayer.Recipes; +using Moryx.Logging; using Moryx.Runtime.Modules; using Moryx.Workplans; @@ -31,6 +32,8 @@ internal class ProductManagementFacade : IFacadeControl, IProductManagement public ModuleConfig Config { get; set; } + public IModuleLogger Logger { get; set; } + #endregion public void Activate() @@ -219,10 +222,15 @@ public ProductInstance GetInstance(IIdentity identity) if (identity == null) throw new ArgumentNullException(nameof(identity)); - var instance = ProductManager - .GetInstances(i => identity.Equals(i.Identity)) - .SingleOrDefault(); - return (ProductInstance) instance; + var instances = ProductManager + .GetInstances(i => identity.Equals(i.Identity)); + if (instances.Count > 1) + { + Logger.Log(LogLevel.Error, "ProductManagement contains more than one {0} with the identity {1}.", nameof(ProductInstance), identity); + throw new InvalidOperationException(); + } + + return (ProductInstance) instances.SingleOrDefault(); ; } public TInstance GetInstance(Expression> selector) diff --git a/src/Moryx.Products.Management/Implementation/Storage/ProductStorage.cs b/src/Moryx.Products.Management/Implementation/Storage/ProductStorage.cs index 4daa21158..4d4219e78 100644 --- a/src/Moryx.Products.Management/Implementation/Storage/ProductStorage.cs +++ b/src/Moryx.Products.Management/Implementation/Storage/ProductStorage.cs @@ -17,7 +17,7 @@ using Moryx.Model.Repositories; using Moryx.Tools; using static Moryx.Products.Management.ProductExpressionHelpers; -using Moryx.Products.Management.Implementation.Storage; +using Moryx.Logging; namespace Moryx.Products.Management { @@ -71,6 +71,11 @@ internal class ProductStorage : IProductStorage, IConfiguredTypesProvider /// public ModuleConfig Config { get; set; } + /// + /// Logger for the product manager module + /// + public IModuleLogger Logger { get; set; } + /// /// Start the storage and load the type strategies /// @@ -770,6 +775,8 @@ private IReadOnlyList LoadInstancesByType(Expression false; } @@ -877,6 +884,13 @@ private void TransformInstance(IUnitOfWork uow, ProductInstanceEntity entity, Pr // Update all parts that are also present as entities foreach (var partEntity in partEntityGroups[partGroup.Key.Name]) { + if (!partGroup.Value.Any()) + { + Logger.Log(LogLevel.Warning, "No reconstruction of the property {1} possible. You have configured the {0} strategy, but the property was null." + + "Please initialize the property in the Initialize method or select the {2} strategy.", + nameof(PartSourceStrategy.FromPartlink), partGroup.Key.Name, nameof(PartSourceStrategy.FromEntities)); + continue; + } var part = partGroup.Value.First(p => p.PartLink.Id == partEntity.PartLinkEntityId); TransformInstance(uow, partEntity, part); } @@ -891,7 +905,7 @@ private void TransformInstance(IUnitOfWork uow, ProductInstanceEntity entity, Pr partArticles[index].PartLink = partLinks.Find(pl => pl?.Id == partCollection[index].PartLinkEntityId.Value); } - if (typeof(ProductInstance).IsAssignableFrom(partGroup.Key.PropertyType) && partArticles.Length == 0) + if (typeof(ProductInstance).IsAssignableFrom(partGroup.Key.PropertyType) && partArticles.Length == 1) { partGroup.Key.SetValue(productInstance, partArticles[0]); } From 91983cfc33006c88522e70a0d3f3236987392c27 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Mon, 29 Jan 2024 06:36:59 +0100 Subject: [PATCH 06/82] Port AbstractionLayer PR 221 to release 6 --- .../Facades/ProductManagementFacade.cs | 6 ++++-- .../Implementation/Storage/ProductStorage.cs | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Moryx.Products.Management/Facades/ProductManagementFacade.cs b/src/Moryx.Products.Management/Facades/ProductManagementFacade.cs index aa66686d1..2acd6c0c2 100644 --- a/src/Moryx.Products.Management/Facades/ProductManagementFacade.cs +++ b/src/Moryx.Products.Management/Facades/ProductManagementFacade.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Moryx.AbstractionLayer.Identity; using Moryx.AbstractionLayer.Products; using Moryx.AbstractionLayer.Recipes; @@ -226,8 +227,9 @@ public ProductInstance GetInstance(IIdentity identity) .GetInstances(i => identity.Equals(i.Identity)); if (instances.Count > 1) { - Logger.Log(LogLevel.Error, "ProductManagement contains more than one {0} with the identity {1}.", nameof(ProductInstance), identity); - throw new InvalidOperationException(); + var ex = new InvalidOperationException($"ProductManagement contains more than one {nameof(ProductInstance)} with the identity {identity}."); + Logger.LogError(ex, "Please make sure that an identity is unique."); + throw ex; } return (ProductInstance) instances.SingleOrDefault(); ; diff --git a/src/Moryx.Products.Management/Implementation/Storage/ProductStorage.cs b/src/Moryx.Products.Management/Implementation/Storage/ProductStorage.cs index 4d4219e78..6cc22f922 100644 --- a/src/Moryx.Products.Management/Implementation/Storage/ProductStorage.cs +++ b/src/Moryx.Products.Management/Implementation/Storage/ProductStorage.cs @@ -18,6 +18,8 @@ using Moryx.Tools; using static Moryx.Products.Management.ProductExpressionHelpers; using Moryx.Logging; +using Moryx.Products.Management.Implementation.Storage; +using Microsoft.Extensions.Logging; namespace Moryx.Products.Management { @@ -775,7 +777,7 @@ private IReadOnlyList LoadInstancesByType(Expression false; @@ -886,9 +888,9 @@ private void TransformInstance(IUnitOfWork uow, ProductInstanceEntity entity, Pr { if (!partGroup.Value.Any()) { - Logger.Log(LogLevel.Warning, "No reconstruction of the property {1} possible. You have configured the {0} strategy, but the property was null." + + Logger.LogWarning("No reconstruction of the property {1} possible. You have configured the {0} strategy, but the property was null." + "Please initialize the property in the Initialize method or select the {2} strategy.", - nameof(PartSourceStrategy.FromPartlink), partGroup.Key.Name, nameof(PartSourceStrategy.FromEntities)); + nameof(PartSourceStrategy.FromPartLink), partGroup.Key.Name, nameof(PartSourceStrategy.FromEntities)); continue; } var part = partGroup.Value.First(p => p.PartLink.Id == partEntity.PartLinkEntityId); From f63bce0f9e34e6f80910111da527c5316f64a528 Mon Sep 17 00:00:00 2001 From: Matho Camara Date: Fri, 2 Feb 2024 09:37:49 +0100 Subject: [PATCH 07/82] Bump version 8.0.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index cd1d2e94f..8b22a322d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.1 +8.0.2 From 62ef8d8f4c70ae017e6e92aed3782bc1ad1b02bb Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Mon, 12 Feb 2024 15:18:46 +0100 Subject: [PATCH 08/82] Adds the AllowMultiple flag to the ResourceAvailableAs attribute When searching for the relevant interfaces to build the resource proxies in the ResourceTypeController, the method creates a list of distinct instanzes of the attribute. However, without the AllowMultiple flag on the attribute this call will never return more than one element. Therefore, the missing flag is added. --- .../ResourceAvailableAsAttribute.cs | 2 +- .../Mocks/DerivedResourceWithNewProxy.cs | 20 +++++++++++++++++++ .../TypeControllerTests.cs | 15 ++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/Tests/Moryx.Resources.Management.Tests/Mocks/DerivedResourceWithNewProxy.cs diff --git a/src/Moryx.AbstractionLayer/Resources/Attributes/ResourceAvailableAsAttribute.cs b/src/Moryx.AbstractionLayer/Resources/Attributes/ResourceAvailableAsAttribute.cs index 026ef4310..db7b6a9f5 100644 --- a/src/Moryx.AbstractionLayer/Resources/Attributes/ResourceAvailableAsAttribute.cs +++ b/src/Moryx.AbstractionLayer/Resources/Attributes/ResourceAvailableAsAttribute.cs @@ -8,7 +8,7 @@ namespace Moryx.AbstractionLayer.Resources /// /// Members of the given interfaces are available outside of the resource management /// - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class ResourceAvailableAsAttribute : Attribute { /// diff --git a/src/Tests/Moryx.Resources.Management.Tests/Mocks/DerivedResourceWithNewProxy.cs b/src/Tests/Moryx.Resources.Management.Tests/Mocks/DerivedResourceWithNewProxy.cs new file mode 100644 index 000000000..0c3197ba0 --- /dev/null +++ b/src/Tests/Moryx.Resources.Management.Tests/Mocks/DerivedResourceWithNewProxy.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using Moryx.AbstractionLayer.Resources; + +namespace Moryx.Resources.Management.Tests +{ + public interface ISecondNonResourceInterface + { + } + + [ResourceAvailableAs(typeof(ISecondNonResourceInterface))] + public class DerivedResourceWithNewProxy : SimpleResource, ISecondNonResourceInterface + { + public override int MultiplyFoo(int factor) + { + return Foo *= factor + 2; + } + } +} diff --git a/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs b/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs index 13513e1a1..9c32d9f15 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs @@ -79,6 +79,21 @@ public void UseBaseProxyForDerivedType() Assert.AreEqual(baseProxy.GetType(), proxy.GetType()); } + [Test] + public void UseNewProxyForDerivedTypeWithNewInterface() + { + // Arrange: Create instance + var baseInstance = new SimpleResource { Id = 2 }; + var instance = new DerivedResourceWithNewProxy { Id = 3 }; + + // Act: Build Proxy + var baseProxy = _typeController.GetProxy(baseInstance); + var proxy = _typeController.GetProxy(instance); + + // Assert: Make sure proxy is still the base type + Assert.That(baseProxy.GetType(), Is.Not.EqualTo(proxy.GetType())); + } + [Test] public void CallMethodOnProxy() { From b341415c89dbb0b7e39d765f559ce91b1d5e8277 Mon Sep 17 00:00:00 2001 From: jsonBackup Date: Sun, 18 Feb 2024 13:12:13 +0100 Subject: [PATCH 09/82] Rebase release/6 --- src/Moryx.CommandCenter.Web/package.json | 85 +++-- .../components/Menu/RoutingMenuItem.tsx | 4 +- .../common/components/Panels/WrapPanel.tsx | 4 +- .../src/common/container/App.tsx | 39 +-- .../src/common/redux/AppState.ts | 8 - .../src/common/redux/CommonActions.ts | 5 - .../src/common/redux/CommonState.ts | 28 +- .../dashboard/components/CPUMemoryChart.tsx | 75 ----- .../src/dashboard/components/Module.tsx | 62 ---- .../src/dashboard/container/Dashboard.tsx | 140 -------- .../src/dashboard/redux/DashboardActions.ts | 31 -- .../src/dashboard/redux/DashboardState.ts | 52 --- .../src/databases/container/DatabaseModel.tsx | 304 +++++++++--------- .../src/databases/container/Databases.tsx | 58 ++-- .../src/log/api/LogRestClient.ts | 46 --- .../api/requests/AddRemoteAppenderRequest.ts | 10 - .../log/api/requests/SetLogLevelRequest.ts | 9 - .../log/api/responses/AddAppenderResponse.ts | 3 - .../src/log/components/LogMenuItemContent.tsx | 55 ---- .../src/log/components/Logger.tsx | 210 ------------ .../src/log/container/Log.tsx | 216 ------------- .../converter/LogLevelToCssClassConverter.ts | 33 -- .../src/log/images/Debug.png | Bin 426 -> 0 bytes .../src/log/images/Error.png | Bin 282 -> 0 bytes .../src/log/images/Fatal.png | Bin 304 -> 0 bytes .../src/log/images/Info.png | Bin 261 -> 0 bytes .../src/log/images/Trace.png | Bin 259 -> 0 bytes .../src/log/images/Warning.png | Bin 436 -> 0 bytes .../src/log/models/LogLevel.ts | 12 - .../src/log/models/LogMenuItem.ts | 10 - .../src/log/models/LogMessageModel.ts | 14 - .../src/log/models/LoggerModel.ts | 17 - .../src/log/redux/LogActions.ts | 12 - .../src/log/redux/LogState.ts | 28 -- .../components/ConfigEditor/BooleanEditor.tsx | 10 +- .../CollapsibleEntryEditorBase.tsx | 2 +- .../ConfigEditor/NavigableConfigEditor.tsx | 22 +- .../src/modules/container/Module.tsx | 56 ++-- .../modules/container/ModuleConfiguration.tsx | 44 ++- .../src/modules/container/ModuleConsole.tsx | 32 +- .../src/modules/container/Modules.tsx | 101 +++--- src/Moryx.CommandCenter.Web/webpack.config.js | 37 +-- .../webpack.dev.config.js | 6 +- .../webpack.prod.config.js | 6 +- 44 files changed, 403 insertions(+), 1483 deletions(-) delete mode 100644 src/Moryx.CommandCenter.Web/src/dashboard/components/CPUMemoryChart.tsx delete mode 100644 src/Moryx.CommandCenter.Web/src/dashboard/components/Module.tsx delete mode 100644 src/Moryx.CommandCenter.Web/src/dashboard/container/Dashboard.tsx delete mode 100644 src/Moryx.CommandCenter.Web/src/dashboard/redux/DashboardActions.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/dashboard/redux/DashboardState.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/api/LogRestClient.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/api/requests/AddRemoteAppenderRequest.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/api/requests/SetLogLevelRequest.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/api/responses/AddAppenderResponse.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/components/LogMenuItemContent.tsx delete mode 100644 src/Moryx.CommandCenter.Web/src/log/components/Logger.tsx delete mode 100644 src/Moryx.CommandCenter.Web/src/log/container/Log.tsx delete mode 100644 src/Moryx.CommandCenter.Web/src/log/converter/LogLevelToCssClassConverter.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/images/Debug.png delete mode 100644 src/Moryx.CommandCenter.Web/src/log/images/Error.png delete mode 100644 src/Moryx.CommandCenter.Web/src/log/images/Fatal.png delete mode 100644 src/Moryx.CommandCenter.Web/src/log/images/Info.png delete mode 100644 src/Moryx.CommandCenter.Web/src/log/images/Trace.png delete mode 100644 src/Moryx.CommandCenter.Web/src/log/images/Warning.png delete mode 100644 src/Moryx.CommandCenter.Web/src/log/models/LogLevel.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/models/LogMenuItem.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/models/LogMessageModel.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/models/LoggerModel.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/redux/LogActions.ts delete mode 100644 src/Moryx.CommandCenter.Web/src/log/redux/LogState.ts diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index 26f1f6f8c..b5dce870e 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -12,57 +12,48 @@ "author": "mma", "license": "Apache-2.0", "dependencies": { - "@mdi/js": "^5.0.45", - "@mdi/react": "^1.3.0", - "@types/chart.js": "^2.7.50", - "@types/history": "^4.7.6", - "@types/prop-types": "^15.7.0", - "@types/query-string": "^5.1.0", - "@types/react": "^16.9.25", - "@types/react-dom": "^16.8.3", - "@types/react-notification-system": "^0.2.39", - "@types/react-redux": "^7.0.5", - "@types/reactstrap": "^7.1.3", - "@types/uuid": "^3.4.4", - "bootstrap": "4.3.1", - "bootstrap-toggle": "^2.2.2", - "chart.js": "^2.8.0", - "history": "^4.10.0", - "jquery": "^1.9.1", - "moment": "^2.24.0", - "query-string": "^6.4.0", - "react": "16.8.5", + "@mdi/js": "^7.4.47", + "@mdi/react": "^1.6.1", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@types/react-redux": "^7.1.33", + "@types/uuid": "^9.0.8", + "bootstrap": "4.6.2", + "bootstrap5-toggle": "^5.0.6", + "history": "^4.10.1", + "moment": "^2.30.1", + "query-string": "^8.2.0", + "react": "18.2.0", "react-bootstrap-toggle": "^2.3.2", - "react-chartjs-2": "^2.7.4", - "react-dom": "16.8.5", - "react-notification-system": "^0.2.17", - "react-redux": "^6.0.1", - "react-router": "^5.3.3", - "react-router-dom": "^5.3.0", + "react-dom": "18.2.0", + "react-redux": "^9.1.0", + "react-router": "^5.3.4", + "react-router-dom": "^5.3.4", "react-router-redux": "^4.0.8", - "reactstrap": "^7.1.0", - "redux": "^4.0.1", - "ts-loader": "^8.1.0", - "uuid": "^9.0.0" + "react-toastify": "^9.1.3", + "reactstrap": "^8.10.1", + "redux": "^5.0.1", + "ts-loader": "^9.5.1", + "uuid": "^9.0.1" }, "devDependencies": { - "@types/react-router": "^5.1.18", + "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", - "@types/react-router-redux": "^5.0.21", - "css-loader": "^2.1.1", - "html-webpack-plugin": "^3.2.0", - "node-sass": "^6.0.0", - "sass-loader": "^10.2.1", - "source-map-loader": "^0.2.4", - "style-loader": "^0.23.1", - "tslint": "^5.14.0", + "@types/react-router-redux": "^5.0.27", + "css-loader": "^6.10.0", + "html-webpack-plugin": "^5.6.0", + "node-sass": "^9.0.0", + "sass-loader": "^14.1.0", + "source-map-loader": "^5.0.0", + "style-loader": "^3.3.4", + "tslint": "^6.1.3", "tslint-loader": "^3.5.4", - "tslint-react": "^3.6.0", - "typescript": "^4.8.3", - "url-loader": "^1.1.2", - "webpack": "^4.29.6", - "webpack-cli": "^3.3.0", - "webpack-dev-server": "^3.2.1", - "webpack-merge": "^4.1.2" + "tslint-react": "^5.0.0", + "typescript": "^5.3.3", + "url-loader": "^4.1.1", + "webpack": "^5.90.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.0", + "webpack-merge": "^5.10.0" } -} +} \ No newline at end of file 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 6cb7b5887..3b86e44d0 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/Menu/RoutingMenuItem.tsx @@ -6,7 +6,7 @@ import { Location, UnregisterCallback } from "history"; import * as React from "react"; import { Link, RouteComponentProps, withRouter } from "react-router-dom"; -import ListGroupItem from "reactstrap/lib/ListGroupItem"; +import { ListGroupItem } from "reactstrap"; import MenuItemModel from "../../models/MenuItemModel"; interface MenuItemProps { @@ -23,7 +23,7 @@ class RoutingMenuItem extends React.Component & MenuItem private unregisterListenerCallback: UnregisterCallback; constructor(props: RouteComponentProps<{}> & MenuItemProps) { - super (props); + super(props); this.state = { IsOpened: this.isOpened(this.props.location) }; this.unregisterListenerCallback = this.props.history.listen(this.onRouteChanged.bind(this)); diff --git a/src/Moryx.CommandCenter.Web/src/common/components/Panels/WrapPanel.tsx b/src/Moryx.CommandCenter.Web/src/common/components/Panels/WrapPanel.tsx index e1ca6f00c..09f1869c9 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/Panels/WrapPanel.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/Panels/WrapPanel.tsx @@ -10,8 +10,8 @@ export interface WrapPanelProps extends React.HTMLAttributes { children?: React.ReactNode; } -const WrapPanel: React.StatelessComponent = (props, {children}) => { - return
{props.children}
; +const WrapPanel: React.FunctionComponent = (props) => { + return
{props.children}
; }; export default WrapPanel; diff --git a/src/Moryx.CommandCenter.Web/src/common/container/App.tsx b/src/Moryx.CommandCenter.Web/src/common/container/App.tsx index 3efcd1270..63d2542a7 100644 --- a/src/Moryx.CommandCenter.Web/src/common/container/App.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/container/App.tsx @@ -4,13 +4,13 @@ */ import * as React from "react"; -import NotificationSystem = require("react-notification-system"); import { connect } from "react-redux"; import { Redirect, Route, RouteComponentProps, Switch, withRouter } from "react-router-dom"; +import { toast, 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 LogRestClient from "../../log/api/LogRestClient"; import ModulesRestClient from "../../modules/api/ModulesRestClient"; import Modules from "../../modules/container/Modules"; import { ModuleServerModuleState } from "../../modules/models/ModuleServerModuleState"; @@ -23,7 +23,7 @@ import ApplicationLoadResponse from "../api/responses/ApplicationLoadResponse"; import HostInformationResponse from "../api/responses/HostInformationResponse"; import SystemLoadResponse from "../api/responses/SystemLoadResponse"; import { AppState } from "../redux/AppState"; -import { updateIsConnected, updateNotificationInstance, updateServerTime } from "../redux/CommonActions"; +import { updateIsConnected, updateServerTime } from "../redux/CommonActions"; import { ActionType } from "../redux/Types"; import "../scss/commandcenter.scss"; @@ -31,7 +31,6 @@ interface AppPropModel { ModulesRestClient: ModulesRestClient; CommonRestClient: CommonRestClient; DatabasesRestClient: DatabasesRestClient; - LogRestClient: LogRestClient; IsConnected: boolean; ShowWaitDialog: boolean; Modules: ServerModuleModel[]; @@ -47,7 +46,6 @@ interface AppDispatchPropModel { onUpdateModuleHealthState?(moduleName: string, healthState: ModuleServerModuleState): void; onUpdateModuleNotifications?(moduleName: string, notifications: NotificationModel[]): void; onUpdateIsConnected?(isConnected: boolean): void; - onUpdateNotificationSystemInstance?(notificationSystem: NotificationSystem): void; } const mapStateToProps = (state: AppState): AppPropModel => { @@ -55,7 +53,6 @@ const mapStateToProps = (state: AppState): AppPropModel => { ModulesRestClient: state.Modules.RestClient, CommonRestClient: state.Common.RestClient, DatabasesRestClient: state.Databases.RestClient, - LogRestClient: state.Log.RestClient, IsConnected: state.Common.IsConnected, ShowWaitDialog: state.Common.ShowWaitDialog, Modules: state.Modules.Modules, @@ -64,22 +61,19 @@ const mapStateToProps = (state: AppState): AppPropModel => { const mapDispatchToProps = (dispatch: React.Dispatch>): AppDispatchPropModel => { return { - onUpdateServerTime: (serverTime: string) => dispatch(updateServerTime(serverTime)), - onUpdateModules: (modules: ServerModuleModel[]) => dispatch(updateModules(modules)), - onUpdateModuleHealthState: (moduleName: string, healthState: ModuleServerModuleState) => - dispatch(updateHealthState(moduleName, healthState)), - onUpdateModuleNotifications: (moduleName: string, notifications: NotificationModel[]) => - dispatch(updateNotifications(moduleName, notifications)), - onUpdateIsConnected: (isConnected: boolean) => dispatch(updateIsConnected(isConnected)), - onUpdateNotificationSystemInstance: (notificationSystem: NotificationSystem) => - dispatch(updateNotificationInstance(notificationSystem)), + onUpdateServerTime: (serverTime: string) => dispatch(updateServerTime(serverTime)), + onUpdateModules: (modules: ServerModuleModel[]) => dispatch(updateModules(modules)), + onUpdateModuleHealthState: (moduleName: string, healthState: ModuleServerModuleState) => + dispatch(updateHealthState(moduleName, healthState)), + onUpdateModuleNotifications: (moduleName: string, notifications: NotificationModel[]) => + dispatch(updateNotifications(moduleName, notifications)), + onUpdateIsConnected: (isConnected: boolean) => dispatch(updateIsConnected(isConnected)) }; }; class App extends React.Component & AppDispatchPropModel> { private updateClockTimer: NodeJS.Timeout; private updateLoadAndModulesTimer: NodeJS.Timeout; - private notificationSystem: NotificationSystem = null; constructor(props: AppPropModel & RouteComponentProps<{}> & AppDispatchPropModel) { super(props); @@ -98,23 +92,16 @@ class App extends React.Component & AppDi } public render(): React.ReactNode { - const ref = (instance: NotificationSystem) => { - if (this.notificationSystem == null) { - this.notificationSystem = instance; - this.props?.onUpdateNotificationSystemInstance(instance); - } - }; - return (
- + - + } />
@@ -135,5 +122,5 @@ class App extends React.Component & AppDi } export default withRouter, React.ComponentType>( -connect(mapStateToProps, mapDispatchToProps)(App) + connect(mapStateToProps, mapDispatchToProps)(App) ); diff --git a/src/Moryx.CommandCenter.Web/src/common/redux/AppState.ts b/src/Moryx.CommandCenter.Web/src/common/redux/AppState.ts index bc7ea84f2..46fb9b675 100644 --- a/src/Moryx.CommandCenter.Web/src/common/redux/AppState.ts +++ b/src/Moryx.CommandCenter.Web/src/common/redux/AppState.ts @@ -3,35 +3,27 @@ * Licensed under the Apache License, Version 2.0 */ -import { DashboardState, getDashboardReducer, initialDashboardState } from "../../dashboard/redux/DashboardState"; import { DatabaseState, getDatabaseReducer, initialDatabaseState } from "../../databases/redux/DatabaseState"; -import { getLogReducer, initialLogState, LogState } from "../../log/redux/LogState"; import { getModulesReducer, initialModulesState, ModulesState } from "../../modules/redux/ModulesState"; import { CommonState, getCommonReducer, initialCommonState } from "./CommonState"; import { ActionType } from "./Types"; export interface AppState { Common: CommonState; - Dashboard: DashboardState; Modules: ModulesState; Databases: DatabaseState; - Log: LogState; } export const initialAppState: AppState = { Common: initialCommonState, - Dashboard: initialDashboardState, Modules: initialModulesState, Databases: initialDatabaseState, - Log: initialLogState, }; export function getAppReducer(state: AppState = initialAppState, action: ActionType<{}>): AppState { return { Common: getCommonReducer(state.Common, action), - Dashboard: getDashboardReducer(state.Dashboard, action), Modules: getModulesReducer(state.Modules, action), Databases: getDatabaseReducer(state.Databases, action), - Log: getLogReducer(state.Log, action), }; } diff --git a/src/Moryx.CommandCenter.Web/src/common/redux/CommonActions.ts b/src/Moryx.CommandCenter.Web/src/common/redux/CommonActions.ts index 5bb2ca7f4..8283c57d0 100644 --- a/src/Moryx.CommandCenter.Web/src/common/redux/CommonActions.ts +++ b/src/Moryx.CommandCenter.Web/src/common/redux/CommonActions.ts @@ -3,7 +3,6 @@ * Licensed under the Apache License, Version 2.0 */ -import NotificationSystem = require("react-notification-system"); import { ActionType } from "./Types"; export const UPDATE_SERVER_TIME = "UPDATE_SERVER_TIME"; @@ -19,10 +18,6 @@ export function updateIsConnected(isConnected: boolean): ActionType { return { type: UPDATE_IS_CONNECTED, payload: isConnected }; } -export function updateNotificationInstance(notificationSystem: NotificationSystem): ActionType { - return { type: UPDATE_NOTIFICATION_INSTANCE, payload: notificationSystem }; -} - export function updateShowWaitDialog(showWaitDialog: boolean): ActionType { return { type: UPDATE_SHOW_WAIT_DIALOG, payload: showWaitDialog }; } diff --git a/src/Moryx.CommandCenter.Web/src/common/redux/CommonState.ts b/src/Moryx.CommandCenter.Web/src/common/redux/CommonState.ts index 944e2a491..25a6d9645 100644 --- a/src/Moryx.CommandCenter.Web/src/common/redux/CommonState.ts +++ b/src/Moryx.CommandCenter.Web/src/common/redux/CommonState.ts @@ -4,7 +4,6 @@ */ require("../../types/constants"); -import NotificationSystem = require("react-notification-system"); import CommonRestClient from "../api/CommonRestClient"; import { UPDATE_IS_CONNECTED, UPDATE_NOTIFICATION_INSTANCE, UPDATE_SERVER_TIME, UPDATE_SHOW_WAIT_DIALOG } from "./CommonActions"; import { ActionType } from "./Types"; @@ -14,31 +13,26 @@ export interface CommonState { ServerTime: string; RestClient: CommonRestClient; ShowWaitDialog: boolean; - NotificationSystem: NotificationSystem; } export const initialCommonState: CommonState = { IsConnected: true, - NotificationSystem: null, RestClient: new CommonRestClient(BASE_URL), ServerTime: "", ShowWaitDialog: false, }; export function getCommonReducer(state: CommonState = initialCommonState, action: ActionType<{}>): CommonState { - switch (action.type) { - case UPDATE_SERVER_TIME: { - return { ...state, ServerTime: action.payload as string }; + switch (action.type) { + case UPDATE_SERVER_TIME: { + return { ...state, ServerTime: action.payload as string }; + } + case UPDATE_IS_CONNECTED: { + return { ...state, IsConnected: action.payload as boolean }; + } + case UPDATE_SHOW_WAIT_DIALOG: { + return { ...state, ShowWaitDialog: action.payload as boolean }; + } } - case UPDATE_IS_CONNECTED: { - return { ...state, IsConnected: action.payload as boolean }; - } - case UPDATE_NOTIFICATION_INSTANCE: { - return { ...state, NotificationSystem: action.payload as NotificationSystem }; - } - case UPDATE_SHOW_WAIT_DIALOG: { - return { ...state, ShowWaitDialog: action.payload as boolean }; - } - } - return state; + return state; } diff --git a/src/Moryx.CommandCenter.Web/src/dashboard/components/CPUMemoryChart.tsx b/src/Moryx.CommandCenter.Web/src/dashboard/components/CPUMemoryChart.tsx deleted file mode 100644 index 188faba47..000000000 --- a/src/Moryx.CommandCenter.Web/src/dashboard/components/CPUMemoryChart.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import * as chartjs from "chart.js"; -import * as React from "react"; -import { ChartData, Line } from "react-chartjs-2"; -import SystemLoadSample from "../../common/models/SystemLoadSample"; - -interface CPUMemoryPropsModel { - Samples: SystemLoadSample[]; -} - -interface CPUMemoryStateModel { - chartData: any; -} - -export class CPUMemoryChart extends React.Component { - constructor(props: CPUMemoryPropsModel) { - super(props); - this.state = { - chartData: CPUMemoryChart.createChartData(), - }; - } - - public componentDidMount(): void { - this.updateChartData(this.props); - } - - public componentWillReceiveProps(nextProps: CPUMemoryPropsModel): void { - this.updateChartData(nextProps); - } - - public updateChartData(nextProps: CPUMemoryPropsModel): void { - const chartData = CPUMemoryChart.createChartData(); - nextProps.Samples.forEach((sample) => { - chartData.labels.push(sample.Date); - chartData.datasets[0].data.push(sample.CPULoad); - chartData.datasets[1].data.push(sample.SystemMemoryLoad); - }); - - this.setState({chartData}); - } - - private static createChartData(): any { - const chartData: ChartData = { - labels: [], - datasets: - [ - { - label: "CPU load (%)", - fill: false, - lineTension: 0.1, - backgroundColor: "rgba(218, 165, 32, 0.5)", - data: [], - }, - { - label: "Used memory (%)", - fill: false, - lineTension: 0.1, - backgroundColor: "rgba(0, 0, 255, 0.5)", - data: [], - }, - ], - }; - return chartData; - } - - public render(): React.ReactNode { - return ( - - ); - } -} diff --git a/src/Moryx.CommandCenter.Web/src/dashboard/components/Module.tsx b/src/Moryx.CommandCenter.Web/src/dashboard/components/Module.tsx deleted file mode 100644 index 142f348ef..000000000 --- a/src/Moryx.CommandCenter.Web/src/dashboard/components/Module.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import { mdiHexagon } from "@mdi/js"; -import Icon from "@mdi/react"; -import * as React from "react"; -import { Link } from "react-router-dom"; -import { Card, CardBody, CardHeader, Col, Container, Row } from "reactstrap"; -import ServerModuleModel from "../../modules/models/ServerModuleModel"; -import { HealthStateToCssClassConverter } from "../converter/HealthStateToCssClassConverter"; -import { HealthStateBadge } from "./HealthStateBadge"; - -interface ModulePropsModel { - ServerModule: ServerModuleModel; -} - -export class Module extends React.Component { - - constructor(props: ModulePropsModel) { - super(props); - this.state = {}; - } - - public render(): React.ReactNode { - const stateCssClass = HealthStateToCssClassConverter.Convert(this.props.ServerModule.healthState); - - return ( -
- - - - - {this.props.ServerModule.name} - - - - - - State: - - - - Assembly: - {this.props.ServerModule.assembly.name} - - - Version: - {this.props.ServerModule.assembly.version} - - - Info: - {this.props.ServerModule.assembly.informationalVersion} - - - - -
- ); - } -} diff --git a/src/Moryx.CommandCenter.Web/src/dashboard/container/Dashboard.tsx b/src/Moryx.CommandCenter.Web/src/dashboard/container/Dashboard.tsx deleted file mode 100644 index dfe3a9c9e..000000000 --- a/src/Moryx.CommandCenter.Web/src/dashboard/container/Dashboard.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import { mdiViewDashboard } from "@mdi/js"; -import Icon from "@mdi/react"; -import * as React from "react"; -import { connect } from "react-redux"; -import { Card, CardBody, CardHeader, Col, Container, Progress, Row } from "reactstrap"; -import CommonRestClient from "../../common/api/CommonRestClient"; -import ApplicationInformationResponse from "../../common/api/responses/ApplicationInformationResponse"; -import ApplicationLoadResponse from "../../common/api/responses/ApplicationLoadResponse"; -import HostInformationResponse from "../../common/api/responses/HostInformationResponse"; -import WrapPanel from "../../common/components/Panels/WrapPanel"; -import kbToString from "../../common/converter/ByteConverter"; -import SystemLoadSample from "../../common/models/SystemLoadSample"; -import { AppState } from "../../common/redux/AppState"; -import { ActionType } from "../../common/redux/Types"; -import ServerModuleModel from "../../modules/models/ServerModuleModel"; -import { updateModules } from "../../modules/redux/ModulesActions"; -import { CPUMemoryChart } from "../components/CPUMemoryChart"; -import { Module } from "../components/Module"; - -interface DashboardPropModel { - ApplicationInfo: ApplicationInformationResponse; - HostInfo: HostInformationResponse; - ApplicationLoad: ApplicationLoadResponse; - SystemLoad: SystemLoadSample[]; - Modules: ServerModuleModel[]; - RestClient: CommonRestClient; -} - -const mapStateToProps = (state: AppState): DashboardPropModel => { - return { - ApplicationInfo: state.Dashboard.ApplicationInfo, - HostInfo: state.Dashboard.HostInfo, - ApplicationLoad: state.Dashboard.ApplicationLoad, - SystemLoad: state.Dashboard.SystemLoad, - Modules: state.Modules.Modules, - RestClient: state.Common.RestClient, - }; -}; - -class Dashboard extends React.Component { - private restCallDate: number; - - constructor(props: DashboardPropModel) { - super(props); - - this.restCallDate = Date.now(); - } - - public calculateMemoryUsagePercentage(): number { - return Math.round((this.props.ApplicationLoad.workingSet / this.props.ApplicationLoad.systemMemory) * 100.0); - } - - public preRenderModules(): React.ReactNode { - return this.props.Modules.map((module, idx) => ); - } - - public render(): React.ReactNode { - return ( - - - - Dashboard - - - - - -

Application

- - - Name: - {this.props.ApplicationInfo.assemblyProduct} - - - Description: - {this.props.ApplicationInfo.assemblyDescription} - - - Version: - {this.props.ApplicationInfo.assemblyVersion} ({this.props.ApplicationInfo.assemblyInformationalVersion}) - - - CPU usage: - {this.props.ApplicationLoad.cpuLoad}% - - - - - - Memory usage: - {kbToString(this.props.ApplicationLoad.workingSet)} ({kbToString(this.props.ApplicationLoad.systemMemory)}) - - - - - - - -

Server

- - - Hostname: - {this.props.HostInfo.machineName} - - - OS: - {this.props.HostInfo.osInformation} - - - Up since: - {new Date(this.restCallDate - this.props.HostInfo.upTime).toLocaleString()} - - - - -

CPU & Memory

- - -
- - -

Modules

- - {this.preRenderModules()} - - -
-
-
-
- ); - } -} - -export default connect(mapStateToProps)(Dashboard); diff --git a/src/Moryx.CommandCenter.Web/src/dashboard/redux/DashboardActions.ts b/src/Moryx.CommandCenter.Web/src/dashboard/redux/DashboardActions.ts deleted file mode 100644 index 9ea4aeb2f..000000000 --- a/src/Moryx.CommandCenter.Web/src/dashboard/redux/DashboardActions.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import ApplicationInformationResponse from "../../common/api/responses/ApplicationInformationResponse"; -import ApplicationLoadResponse from "../../common/api/responses/ApplicationLoadResponse"; -import HostInformationResponse from "../../common/api/responses/HostInformationResponse"; -import SystemLoadResponse from "../../common/api/responses/SystemLoadResponse"; -import { ActionType } from "../../common/redux/Types"; - -export const UPDATE_APPLICATION_INFO = "UPDATE_APPLICATION_INFO"; -export const UPDATE_HOST_INFO = "UPDATE_HOST_INFO"; -export const UPDATE_APPLICATION_LOAD = "UPDATE_APPLICATION_LOAD"; -export const UPDATE_SYSTEM_LOAD = "UPDATE_SYSTEM_LOAD"; - -export function updateApplicationInfo(applicationInfo: ApplicationInformationResponse): ActionType { - return { type: UPDATE_APPLICATION_INFO, payload: applicationInfo }; -} - -export function updateHostInfo(hostInfo: HostInformationResponse): ActionType { - return { type: UPDATE_HOST_INFO, payload: hostInfo }; -} - -export function updateApplicationLoad(applicationLoad: ApplicationLoadResponse): ActionType { - return { type: UPDATE_APPLICATION_LOAD, payload: applicationLoad }; -} - -export function updateSystemLoad(systemLoad: SystemLoadResponse): ActionType { - return { type: UPDATE_SYSTEM_LOAD, payload: systemLoad }; -} diff --git a/src/Moryx.CommandCenter.Web/src/dashboard/redux/DashboardState.ts b/src/Moryx.CommandCenter.Web/src/dashboard/redux/DashboardState.ts deleted file mode 100644 index 49148a711..000000000 --- a/src/Moryx.CommandCenter.Web/src/dashboard/redux/DashboardState.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import ApplicationInformationResponse from "../../common/api/responses/ApplicationInformationResponse"; -import ApplicationLoadResponse from "../../common/api/responses/ApplicationLoadResponse"; -import HostInformationResponse from "../../common/api/responses/HostInformationResponse"; -import SystemLoadResponse from "../../common/api/responses/SystemLoadResponse"; -import SystemLoadSample from "../../common/models/SystemLoadSample"; -import { ActionType } from "../../common/redux/Types"; -import { UPDATE_APPLICATION_INFO, UPDATE_APPLICATION_LOAD, UPDATE_HOST_INFO, UPDATE_SYSTEM_LOAD } from "./DashboardActions"; - -export interface DashboardState { - ApplicationInfo: ApplicationInformationResponse; - HostInfo: HostInformationResponse; - ApplicationLoad: ApplicationLoadResponse; - SystemLoad: SystemLoadSample[]; -} - -export const initialDashboardState: DashboardState = { - ApplicationInfo: { assemblyProduct: "", assemblyVersion: "", assemblyInformationalVersion: "", assemblyDescription: "" }, - HostInfo: { machineName: "", osInformation: "", upTime: 0 }, - ApplicationLoad: { cpuLoad: 0, systemMemory: 0, workingSet: 0 }, - SystemLoad: [], -}; - -export function getDashboardReducer(state: DashboardState = initialDashboardState, action: ActionType<{}>): DashboardState { - switch (action.type) { - case UPDATE_APPLICATION_INFO: { - return { ...state, ApplicationInfo: action.payload as ApplicationInformationResponse }; - } - case UPDATE_HOST_INFO: { - return { ...state, HostInfo: action.payload as HostInformationResponse }; - } - case UPDATE_APPLICATION_LOAD: { - return { ...state, ApplicationLoad: action.payload as ApplicationLoadResponse }; - } - case UPDATE_SYSTEM_LOAD: { - const systemLoadResponse = action.payload as SystemLoadResponse; - const samples = state.SystemLoad.slice(); - if (samples.length === 5) { - samples.shift(); - } - - samples.push({ Date: new Date(Date.now()).toLocaleString(), CPULoad: systemLoadResponse.cpuLoad, SystemMemoryLoad: systemLoadResponse.systemMemoryLoad }); - - return { ...state, SystemLoad: samples }; - } - } - return state; -} diff --git a/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseModel.tsx b/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseModel.tsx index 5d8f33e17..6794a56c5 100644 --- a/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseModel.tsx +++ b/src/Moryx.CommandCenter.Web/src/databases/container/DatabaseModel.tsx @@ -3,14 +3,14 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiBriefcase, mdiCheck, mdiDatabase, mdiExclamationThick, mdiLoading, mdiPowerPlug, mdiTable} from "@mdi/js"; +import { mdiBriefcase, mdiCheck, mdiDatabase, mdiExclamationThick, mdiLoading, mdiPowerPlug, mdiTable } from "@mdi/js"; import Icon from "@mdi/react"; import * as moment from "moment"; import { any, element, string } from "prop-types"; import * as React from "react"; -import NotificationSystem = require("react-notification-system"); import { connect, Provider } from "react-redux"; import { RouteComponentProps, withRouter } from "react-router-dom"; +import { toast, ToastContainer } 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"; @@ -26,7 +26,6 @@ import { updateDatabaseConfig } from "../redux/DatabaseActions"; interface DatabaseModelPropsModel { RestClient: DatabasesRestClient; DataModel: DataModel; - NotificationSystem: NotificationSystem; } interface DatabaseModelStateModel { @@ -57,10 +56,10 @@ class DatabaseModel extends React.Component): void { - this.setState({selectedMigration: (e.target as HTMLSelectElement).value}); + this.setState({ selectedMigration: (e.target as HTMLSelectElement).value }); } public onSelectSetup(e: React.FormEvent): void { - this.setState({selectedSetup: (e.target as HTMLSelectElement).selectedIndex}); + this.setState({ selectedSetup: (e.target as HTMLSelectElement).selectedIndex }); } public getValidationState(entryName: string) { 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] ? { valid: true, invalid: false } : { invalid: true, valid: false }) : { valid: true, invalid: false }; return 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)}}); + this.setState({ + config: { + ...this.state.config, configuratorTypename: (e.target as HTMLSelectElement).value, + entries: this.getConfigWithDefaultValue((e.target as HTMLSelectElement).value) + } + }); } 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}} + config: { ...this.state.config, entries: { ...this.state.config.entries, [entryName]: (e.target as HTMLSelectElement).value } } }); } public onSelectBackup(e: React.FormEvent): void { - this.setState({selectedBackup: (e.target as HTMLSelectElement).value}); + this.setState({ selectedBackup: (e.target as HTMLSelectElement).value }); } public createEntriesInput() { return Object.keys(this.state.config.entries)?.map((element) => { return ( - this.onTestConnection()} onChange={(e: React.FormEvent) => this.onInputChanged(e, element)}/> - ); - }); + this.onTestConnection()} onChange={(e: React.FormEvent) => this.onInputChanged(e, element)} /> + ); + }); } public getConfigEntries() { @@ -134,9 +137,10 @@ class DatabaseModel extends React.Component { this.props.onShowWaitDialog(false); - this.setState({config: response.config}); + this.setState({ config: response.config }); this.props.onUpdateDatabaseConfig(response); - this.props.NotificationSystem.addNotification({ title: "Configuration saved", message: "", level: "success", autoDismiss: 5 }); + toast.success("Configuration saved", { autoClose: 5000 }); this.onTestConnection(); }).catch((d) => this.props.onShowWaitDialog(false)); } @@ -157,10 +161,12 @@ class DatabaseModel extends React.Component { - this.setState({ testConnectionPending: false, - testConnectionResult: response.result !== undefined ? response.result : TestConnectionResult.ConnectionError }); - }); + .then((response) => { + this.setState({ + testConnectionPending: false, + testConnectionResult: response.result !== undefined ? response.result : TestConnectionResult.ConnectionError + }); + }); } public onCreateDatabase(): void { @@ -171,10 +177,10 @@ class DatabaseModel extends React.Component this.props.onUpdateDatabaseConfig(databaseConfig)); - this.props.NotificationSystem.addNotification({ title: "Success", message: "Database created successfully", level: "success", autoDismiss: 5 }); - } else { - this.props.NotificationSystem.addNotification({ title: "Error", message: "Database not created: " + data.errorMessage, level: "error", autoDismiss: 5 }); - } + toast.success("Database created successfully", { autoClose: 5000 }); + } else { + toast.error("Database not created: " + data.errorMessage, { autoClose: 5000 }); + } this.onTestConnection(); }).catch((d) => this.props.onShowWaitDialog(false)); } @@ -191,9 +197,9 @@ class DatabaseModel extends React.Component this.props.onUpdateDatabaseConfig(databaseConfig)); - this.props.NotificationSystem.addNotification({ title: "Success", message: "Database deleted successfully", level: "success", autoDismiss: 5 }); + toast.success("Database deleted successfully", { autoClose: 5000 }); } else { - this.props.NotificationSystem.addNotification({ title: "Error", message: "Database not deleted: " + data.errorMessage, level: "error", autoDismiss: 5 }); + toast.error("Database not deleted: " + data.errorMessage, { autoClose: 5000 }); } this.onTestConnection(); }).catch((d) => this.props.onShowWaitDialog(false)); @@ -205,9 +211,9 @@ class DatabaseModel extends React.Component { this.props.onShowWaitDialog(false); if (data.success) { - this.props.NotificationSystem.addNotification({ title: "Success", message: "Database dump started successfully. Please refer to the log to get information about the progress.", level: "success", autoDismiss: 5 }); + toast.success("Database dump started successfully. Please refer to the log to get information about the progress.", { autoClose: 5000 }); } else { - this.props.NotificationSystem.addNotification({ title: "Error", message: "Dump start failed: " + data.errorMessage, level: "error", autoDismiss: 5 }); + toast.error("Dump start failed: " + data.errorMessage, { autoClose: 5000 }); } }).catch((d) => this.props.onShowWaitDialog(false)); } @@ -218,9 +224,9 @@ class DatabaseModel extends React.Component { this.props.onShowWaitDialog(false); if (data.success) { - this.props.NotificationSystem.addNotification({ title: "Success", message: "Database restore started successfully. Please refer to the log to get information about the progress.", level: "success", autoDismiss: 5 }); + toast.success("Database restore started successfully. Please refer to the log to get information about the progress.", { autoClose: 5000 }); } else { - this.props.NotificationSystem.addNotification({ title: "Error", message: "Restore start failed: " + data.errorMessage, level: "error", autoDismiss: 5 }); + toast.error("Restore start failed: " + data.errorMessage, { autoClose: 5000 }); } }).catch((d) => this.props.onShowWaitDialog(false)); } @@ -233,9 +239,9 @@ class DatabaseModel extends React.Component this.props.onUpdateDatabaseConfig(databaseConfig)); - this.props.NotificationSystem.addNotification({ title: "Success", message: "Migration applied", level: "success", autoDismiss: 5 }); + toast.success("Migration applied", { autoClose: 5000 }); } else { - this.props.NotificationSystem.addNotification({ title: "Error", message: "Migration not applied", level: "error", autoDismiss: 5 }); + toast.error("Migration not applied", { autoClose: 5000 }); } }).catch((d) => this.props.onShowWaitDialog(false)); } @@ -248,9 +254,9 @@ class DatabaseModel extends React.Component this.props.onUpdateDatabaseConfig(databaseConfig)); - this.props.NotificationSystem.addNotification({ title: "Success", message: "Database rollback completed successfully", level: "success", autoDismiss: 5 }); + toast.success("Database rollback completed successfully", { autoClose: 5000 }); } else { - this.props.NotificationSystem.addNotification({ title: "Error", message: "Database rollback failed: " + data.errorMessage, level: "error", autoDismiss: 5 }); + toast.error("Database rollback failed: " + data.errorMessage, { autoClose: 5000 }); } }).catch((d) => this.props.onShowWaitDialog(false)); } @@ -264,9 +270,9 @@ class DatabaseModel extends React.Component this.props.onShowWaitDialog(false)); } @@ -276,29 +282,29 @@ class DatabaseModel extends React.Component); case TestConnectionResult.ConfigurationError: - return (
- - - - Please check if model configuration exists on server. - -
); + return (
+ + + + Please check if model configuration exists on server. + +
); case TestConnectionResult.ConnectionError: - return (
- - - - Please check Database name and connection string. - -
); + return (
+ + + + Please check Database name and connection string. + +
); 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 (
+ + + + 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. + +
); default: return (
); } @@ -317,9 +323,9 @@ class DatabaseModel extends React.Component

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

@@ -328,10 +334,10 @@ class DatabaseModel extends React.Component - ) => this.onConfiguratorTypeChanged(e)} - value={this.state.config.configuratorTypename} onBlur={() => this.onTestConnection()}> - {this.props.DataModel.possibleConfigurators.map((config, idx) => ())} - + ) => this.onConfiguratorTypeChanged(e)} + value={this.state.config.configuratorTypename} onBlur={() => this.onTestConnection()}> + {this.props.DataModel.possibleConfigurators.map((config, idx) => ())} + {this.state.config.configuratorTypename && this.createEntriesInput()} @@ -347,7 +353,7 @@ class DatabaseModel extends React.Component ) => this.onSelectBackup(e)}> + onChange={(e: React.FormEvent) => this.onSelectBackup(e)}> { this.props.DataModel.backups.map((backup, idx) => { return (); @@ -361,13 +367,13 @@ class DatabaseModel extends React.Component @@ -379,27 +385,27 @@ class DatabaseModel extends React.Component - - -

Database

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

Database

+ +
+ + + + + + + +
@@ -415,41 +421,41 @@ class DatabaseModel extends React.Component - { this.props.DataModel.availableMigrations.length !== 0 ? + {this.props.DataModel.availableMigrations.length !== 0 ? ( - - - - ) => this.onSelectMigration(e)}> - { - this.props.DataModel.availableMigrations.map((migration, idx) => { - const installed = this.props.DataModel.appliedMigrations.find((installedMigration: DbMigrationsModel) => installedMigration.name === migration.name); - const option = migration.name + " (" + (installed ? "Installed" : "Not installed") + ")"; - - return (); - }) - } - - - - - - - - - - - - + Rollback all migrations + + + + + ) : ( @@ -459,31 +465,31 @@ class DatabaseModel extends React.Component - { this.props.DataModel.setups.length !== 0 ? + {this.props.DataModel.setups.length !== 0 ? ( - - - - ) => this.onSelectSetup(e)}> - { - this.props.DataModel.setups.map((setup, idx) => { - return (); - }) - } - - - - - - - - - + Execute setup + + + + ) : ( diff --git a/src/Moryx.CommandCenter.Web/src/databases/container/Databases.tsx b/src/Moryx.CommandCenter.Web/src/databases/container/Databases.tsx index 381276241..a91dc673d 100644 --- a/src/Moryx.CommandCenter.Web/src/databases/container/Databases.tsx +++ b/src/Moryx.CommandCenter.Web/src/databases/container/Databases.tsx @@ -3,17 +3,12 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiBriefcase, mdiComment, mdiDatabase, mdiHexagonMultiple} from "@mdi/js"; +import { mdiBriefcase, mdiComment, mdiDatabase, mdiHexagonMultiple } from "@mdi/js"; import Icon from "@mdi/react"; import * as React from "react"; -import NotificationSystem = require("react-notification-system"); import { connect } from "react-redux"; import { Link, Route, RouteComponentProps, Switch } from "react-router-dom"; -import { Card, CardBody, CardHeader, Col, Row } from "reactstrap"; -import ListGroup from "reactstrap/lib/ListGroup"; -import Nav from "reactstrap/lib/Nav"; -import Navbar from "reactstrap/lib/Navbar"; -import NavItem from "reactstrap/lib/NavItem"; +import { Card, CardBody, CardHeader, Col, ListGroup, Nav, Navbar, NavItem, Row } from "reactstrap"; import RoutingMenu from "../../common/components/Menu/RoutingMenu"; import MenuItemModel from "../../common/models/MenuItemModel"; import MenuModel from "../../common/models/MenuModel"; @@ -27,7 +22,6 @@ import DatabaseModel from "./DatabaseModel"; interface DatabasesPropsModel { RestClient?: DatabasesRestClient; DatabaseConfigs?: DataModel[]; - NotificationSystem?: NotificationSystem; } interface DatabasesDispatchPropModel { @@ -37,8 +31,7 @@ interface DatabasesDispatchPropModel { const mapStateToProps = (state: AppState): DatabasesPropsModel => { return { RestClient: state.Databases.RestClient, - DatabaseConfigs: state.Databases.DatabaseConfigs, - NotificationSystem: state.Common.NotificationSystem, + DatabaseConfigs: state.Databases.DatabaseConfigs }; }; @@ -80,7 +73,7 @@ class Database extends React.Component{namespace}

), + Content: (

{namespace}

), SubMenuItems: [], }; } @@ -90,7 +83,10 @@ class Database extends React.Component { - routes.push( }/>); + routes.push( + + + ); ++idx; }); @@ -103,25 +99,25 @@ class Database extends React.Component - - - + + + - { this.state.IsLoading ? ( + {this.state.IsLoading ? ( Loading... ) : ( @@ -131,7 +127,7 @@ class Database extends React.Component - + @@ -141,7 +137,7 @@ class Database extends React.ComponentConfigure all available database models. Please select a database model to proceed... - } /> + {this.preRenderRoutesList()} diff --git a/src/Moryx.CommandCenter.Web/src/log/api/LogRestClient.ts b/src/Moryx.CommandCenter.Web/src/log/api/LogRestClient.ts deleted file mode 100644 index ba0453105..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/api/LogRestClient.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ -import InvocationResponse from "../../common/api/responses/InvocationResponse"; -import RestClientBase from "../../common/api/RestClientBase"; -import LoggerModel from "../models/LoggerModel"; -import { LogLevel } from "../models/LogLevel"; -import LogMessageModel from "../models/LogMessageModel"; -import AddRemoteAppenderRequest from "./requests/AddRemoteAppenderRequest"; -import SetLogLevelRequest from "./requests/SetLogLevelRequest"; -import AddAppenderResponse from "./responses/AddAppenderResponse"; - -const ROOT_PATH = "/loggers"; -const APPENDER_PATH = ROOT_PATH + "/appenders"; - -export default class LogRestClient extends RestClientBase { - public loggers(): Promise { - return this.get(ROOT_PATH, []); - } - - public addRemoteAppender(name: string, minLevel: LogLevel): Promise { - const request = new AddRemoteAppenderRequest(); - request.MinLevel = minLevel; - request.Name = name; - return this.post(APPENDER_PATH, request, new AddAppenderResponse()); - } - - public removeRemoteAppender(appenderId: number): Promise { - return this.deleteNoBody(LogRestClient.pathToAppender(appenderId), new InvocationResponse()); - } - - public messages(appenderId: number): Promise { - return this.get(LogRestClient.pathToAppender(appenderId), []); - } - - public logLevel(loggerName: string, level: LogLevel): Promise { - const request = new SetLogLevelRequest(); - request.Level = level; - return this.put(ROOT_PATH + `/${loggerName}/loglevel`, request, new InvocationResponse()); - } - - public static pathToAppender(appenderId: number): string { - return APPENDER_PATH + `/${appenderId.toString()}`; - } -} diff --git a/src/Moryx.CommandCenter.Web/src/log/api/requests/AddRemoteAppenderRequest.ts b/src/Moryx.CommandCenter.Web/src/log/api/requests/AddRemoteAppenderRequest.ts deleted file mode 100644 index ec37ca63a..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/api/requests/AddRemoteAppenderRequest.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ -import { LogLevel } from "../../models/LogLevel"; - -export default class AddRemoteAppenderRequest { - public Name: string; - public MinLevel: LogLevel; -} diff --git a/src/Moryx.CommandCenter.Web/src/log/api/requests/SetLogLevelRequest.ts b/src/Moryx.CommandCenter.Web/src/log/api/requests/SetLogLevelRequest.ts deleted file mode 100644 index 88cd3fdf4..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/api/requests/SetLogLevelRequest.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ -import { LogLevel } from "../../models/LogLevel"; - -export default class SetLogLevelRequest { - public Level: LogLevel; -} diff --git a/src/Moryx.CommandCenter.Web/src/log/api/responses/AddAppenderResponse.ts b/src/Moryx.CommandCenter.Web/src/log/api/responses/AddAppenderResponse.ts deleted file mode 100644 index fce69f094..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/api/responses/AddAppenderResponse.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default class AddAppenderResponse { - public appenderId: number; -} diff --git a/src/Moryx.CommandCenter.Web/src/log/components/LogMenuItemContent.tsx b/src/Moryx.CommandCenter.Web/src/log/components/LogMenuItemContent.tsx deleted file mode 100644 index ef0b54986..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/components/LogMenuItemContent.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -const TraceImg = require("../images/Trace.png"); -const DebugImg = require("../images/Debug.png"); -const InfoImg = require("../images/Info.png"); -const WarningImg = require("../images/Warning.png"); -const ErrorImg = require("../images/Error.png"); -const FatalImg = require("../images/Fatal.png"); -import * as React from "react"; -import LoggerModel from "../models/LoggerModel"; -import { LogLevel } from "../models/LogLevel"; - -interface LogMenuItemContentPropsModel { - Logger: LoggerModel; - onActiveLogLevelChange(e: React.FormEvent, l: LoggerModel): void; - onLabelClicked(l: LoggerModel): void; -} - -export default class LogMenuItemContent extends React.Component { - - public render(): React.ReactNode { - let img: any = TraceImg; - switch (this.props.Logger.activeLevel) { - case LogLevel.Trace: img = TraceImg; break; - case LogLevel.Debug: img = DebugImg; break; - case LogLevel.Info: img = InfoImg; break; - case LogLevel.Warning: img = WarningImg; break; - case LogLevel.Error: img = ErrorImg; break; - case LogLevel.Fatal: img = FatalImg; - } - - return ( -
- - ) => this.props.onLabelClicked(this.props.Logger)}> - {LoggerModel.shortLoggerName(this.props.Logger)} - -
- ); - } -} diff --git a/src/Moryx.CommandCenter.Web/src/log/components/Logger.tsx b/src/Moryx.CommandCenter.Web/src/log/components/Logger.tsx deleted file mode 100644 index 1b34747e3..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/components/Logger.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import * as moment from "moment"; -import * as React from "react"; -import NotificationSystem = require("react-notification-system"); -import { Button, ButtonGroup, Col, Container, Input, Modal, ModalBody, ModalFooter, ModalHeader, Row, Table } from "reactstrap"; -import LogRestClient from "../api/LogRestClient"; -import LogLevelToCssClassConverter from "../converter/LogLevelToCssClassConverter"; -import LoggerModel from "../models/LoggerModel"; -import { LogLevel } from "../models/LogLevel"; -import LogMessageModel from "../models/LogMessageModel"; - -interface LogPropsModel { - RestClient: LogRestClient; - Logger: LoggerModel; - onCloseTab(logger: LoggerModel): void; -} - -interface LogStateModel { - LogMessages: LogMessageModel[]; - FilteredLogMessages: LogMessageModel[]; - AppenderId: number; - MaxLogEntries: number; - IntermediateMaxLogEntries: number; - FilterLogLevel: LogLevel; - SelectedLogMessage: LogMessageModel; - IsLogDetailDialogOpen: boolean; -} - -export default class Logger extends React.Component { - private updateLogMessagesTimer: NodeJS.Timeout; - - constructor(props: LogPropsModel) { - super(props); - this.state = { - LogMessages: [], - FilteredLogMessages: [], - AppenderId: -1, - MaxLogEntries: 20, - IntermediateMaxLogEntries: 20, - FilterLogLevel: LogLevel.Info, - SelectedLogMessage: null, - IsLogDetailDialogOpen: false, - }; - } - - public componentDidMount(): void { - this.props.RestClient.addRemoteAppender(this.props.Logger.name, this.props.Logger.activeLevel).then((data) => { - this.setState({ AppenderId: data.appenderId }); - this.onUpdateLogMessages(); - this.updateLogMessagesTimer = setInterval(this.onUpdateLogMessages.bind(this), 3000); - }); - } - - public componentWillUnmount(): void { - clearInterval(this.updateLogMessagesTimer); - if (this.state.AppenderId !== undefined) { - this.props.RestClient.removeRemoteAppender(this.state.AppenderId); - } - } - - public render(): React.ReactNode { - return ( -
- - - Filter by log level: - - ) => this.onFilterLogLevelChange(e)}> - - - - - - - - - Max. log entries: - - ) => this.onChangeMaxEntries(e)} - onBlur={this.onApplyMaxEntries.bind(this)} /> - - - - - - { this.props.onCloseTab != null && - - } - - - - - - - - - - - - - - - - { this.state.LogMessages.length !== 0 && ( - this.preRenderLogMessages() - )} - - { this.state.LogMessages.length === 0 && ( - - - - - - )} -
TimestampLevelMessageClass name
- No log messages found for this logger. -
- -
-
- { this.state.SelectedLogMessage != null && - - - Log message from {moment(this.state.SelectedLogMessage.timestamp).format("YYYY-MM-DD HH:mm:ss")} ({this.state.SelectedLogMessage.className}) - - - - - -
{this.state.SelectedLogMessage.message}
- -
-
-
- - - -
- } -
- ); - } - - private onUpdateLogMessages(): void { - this.props.RestClient.messages(this.state.AppenderId).then((data) => this.updateLogMessages(data)); - } - - private updateLogMessages(logMessages: LogMessageModel[]): void { - logMessages.reverse(); - const newLogMessageList = logMessages.concat(this.state.LogMessages); - this.setState({ LogMessages: newLogMessageList, FilteredLogMessages: Logger.applyFilter(newLogMessageList, this.state.FilterLogLevel, this.state.MaxLogEntries) }); - } - - private clearLogMessages(): void { - this.setState({ LogMessages: [], FilteredLogMessages: [] }); - } - - private onFilterLogLevelChange(e: React.FormEvent): void { - const newValue = (e.target as HTMLSelectElement).value as LogLevel; - this.setState({ FilterLogLevel: newValue, FilteredLogMessages: Logger.applyFilter(this.state.LogMessages, newValue, this.state.MaxLogEntries) }); - } - - private onChangeMaxEntries(e: React.FormEvent): void { - this.setState({ IntermediateMaxLogEntries: parseInt(e.currentTarget.value, 10) }); - } - - private onApplyMaxEntries(): void { - this.setState({ MaxLogEntries: this.state.IntermediateMaxLogEntries, FilteredLogMessages: Logger.applyFilter(this.state.LogMessages, this.state.FilterLogLevel, this.state.IntermediateMaxLogEntries) }); - } - - private static applyFilter(logMessages: LogMessageModel[], logLevel: LogLevel, maxEntries: number): LogMessageModel[] { - return logMessages.filter((logMessage: LogMessageModel) => logMessage.logLevel >= logLevel).slice(0, maxEntries); - } - - private onShowLogMessageDetailed(logMessage: LogMessageModel): void { - this.setState({ SelectedLogMessage: logMessage, IsLogDetailDialogOpen: true }); - } - - private static cutMessage(message: string): string { - const lines: string[] = message.split("\n"); - let cutted = lines.length > 0 ? lines[0] : message; - cutted = cutted.slice(0, 150); - - if (cutted.length < message.length) { - cutted += "..."; - } - - return cutted; - } - - private preRenderLogMessages(): React.ReactNode { - return this.state.FilteredLogMessages.map((message, idx) => - - {moment(message.timestamp).format("YYYY-MM-DD HH:mm:ss")} - {LogLevel[message.logLevel]} - {Logger.cutMessage(message.message)} - {message.className} - , - ); - } -} diff --git a/src/Moryx.CommandCenter.Web/src/log/container/Log.tsx b/src/Moryx.CommandCenter.Web/src/log/container/Log.tsx deleted file mode 100644 index 68d5a8fc3..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/container/Log.tsx +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ - -import { mdiFormatListBulletedSquare, mdiSquareEditOutline } from "@mdi/js"; -import Icon from "@mdi/react"; -import * as React from "react"; -import NotificationSystem = require("react-notification-system"); -import { connect } from "react-redux"; -import { Card, CardBody, CardHeader, Col, Container, Nav, NavItem, NavLink, Row, TabContent, TabPane } from "reactstrap"; -import TreeMenu from "../../common/components/Menu/TreeMenu"; -import MenuModel from "../../common/models/MenuModel"; -import { AppState } from "../../common/redux/AppState"; -import { ActionType } from "../../common/redux/Types"; -import LogRestClient from "../api/LogRestClient"; -import Logger from "../components/Logger"; -import LogMenuItemContent from "../components/LogMenuItemContent"; -import LoggerModel from "../models/LoggerModel"; -import { LogLevel } from "../models/LogLevel"; -import LogMenuItem from "../models/LogMenuItem"; -import { updateLoggers } from "../redux/LogActions"; - -interface LogPropsModel { - RestClient?: LogRestClient; - Loggers?: LoggerModel[]; - NotificationSystem?: NotificationSystem; -} - -interface LogDispatchPropModel { - onUpdateLoggers?(loggers: LoggerModel[]): void; -} - -const mapStateToProps = (state: AppState): LogPropsModel => { - return { - RestClient: state.Log.RestClient, - Loggers: state.Log.Loggers, - NotificationSystem: state.Common.NotificationSystem, - }; -}; - -const mapDispatchToProps = (dispatch: React.Dispatch>): LogDispatchPropModel => { - return { - onUpdateLoggers: (loggers: LoggerModel[]) => dispatch(updateLoggers(loggers)), - }; -}; - -interface LogStateModel { - ActiveTab: string; - Menu: MenuModel; - LoggerTabs: LoggerModel[]; -} - -class Log extends React.Component { - - private overviewLogger: LoggerModel; - - constructor(props: LogPropsModel & LogDispatchPropModel) { - super(props); - this.state = { ActiveTab: "0", Menu: { MenuItems: [] }, LoggerTabs: [] }; - - this.overviewLogger = new LoggerModel(); - this.overviewLogger.name = ""; - } - - public componentDidMount(): void { - this.props.RestClient.loggers().then((data) => { - this.props.onUpdateLoggers(data); - this.setState({Menu: { MenuItems: data.map((logger, idx) => this.createMenuItem(logger)) } }); - }); - } - - public createMenuItem(logger: LoggerModel): LogMenuItem { - const menuItem: LogMenuItem = { - Name: LoggerModel.shortLoggerName(logger), - NavPath: "/log", - Logger: logger, - SubMenuItems: logger.childLogger.map((childLogger, idx) => this.createMenuItem(childLogger)), - }; - - menuItem.Content = (); - return menuItem; - } - - private onActiveLogLevelChange(e: React.FormEvent, logger: LoggerModel): void { - e.preventDefault(); - - const newValue = (e.target as HTMLSelectElement).value as LogLevel; - this.props.RestClient.logLevel(logger.name, newValue).then((data) => { - if (data.success) { - logger.activeLevel = newValue; - Log.changeActiveLogLevel(logger, newValue); - this.forceUpdate(); - - this.props.NotificationSystem.addNotification({ title: "Success", message: "Log level for '" + logger.name + "' was set successfully", level: "success", autoDismiss: 5 }); - } else { - this.props.NotificationSystem.addNotification({ title: "Error", message: data.errorMessage, level: "error", autoDismiss: 5 }); - } - }); - } - - private static changeActiveLogLevel(logger: LoggerModel, logLevel: LogLevel): void { - if (logger.activeLevel > logLevel) { - logger.activeLevel = logLevel; - } - - for (const childLogger of logger.childLogger) { - Log.changeActiveLogLevel(childLogger, logLevel); - } - } - - private toggleTab(tabName: string): void { - this.setState({ActiveTab: tabName}); - } - - private onMenuItemClicked(logger: LoggerModel): void { - const idx = this.state.LoggerTabs.indexOf(logger); - if (idx === -1) { - this.setState((prevState) => ({ - LoggerTabs: [...prevState.LoggerTabs, logger], - ActiveTab: (prevState.LoggerTabs.length + 1).toString(), - })); - } else { - this.setState({ ActiveTab: (idx + 1).toString() }); - } - } - - private onCloseTab(logger: LoggerModel): void { - const idx = this.state.LoggerTabs.indexOf(logger); - if (idx !== -1) { - let activeTab = parseInt(this.state.ActiveTab, 10); - if (activeTab >= this.state.LoggerTabs.length) { - activeTab -= 1; - } - - this.setState((prevState) => ({ - LoggerTabs: prevState.LoggerTabs.filter((_, i) => i !== idx), - ActiveTab: (activeTab).toString(), - })); - } - } - - private preRenderNavForTabs(): React.ReactNode { - return this.state.LoggerTabs.map((logger, idx) => - - { this.toggleTab((idx + 1).toString()); }}> - {LoggerModel.shortLoggerName(logger)} - - , - ); - } - - private preRenderTabs(): React.ReactNode { - return this.state.LoggerTabs.map((logger, idx) => - - - - - - - , - ); - } - - public render(): React.ReactNode { - return ( - - - - - - Loggers - - - - - - - - - - - - - Log - - - - - - - - - - - - {this.preRenderTabs()} - - - - - - ); - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(Log); diff --git a/src/Moryx.CommandCenter.Web/src/log/converter/LogLevelToCssClassConverter.ts b/src/Moryx.CommandCenter.Web/src/log/converter/LogLevelToCssClassConverter.ts deleted file mode 100644 index c1ea9d685..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/converter/LogLevelToCssClassConverter.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ -import { LogLevel } from "../models/LogLevel"; - -export default class LogLevelToCssClassConverter { - public static Convert(logLevel: LogLevel): string { - switch (logLevel) { - case LogLevel.Trace: { - return "#e9e9e9"; - } - case LogLevel.Debug: { - return "#d9d9d9"; - } - case LogLevel.Info: { - return "white"; - } - case LogLevel.Warning: { - return "#ffffcc"; - } - case LogLevel.Error: { - return "#ffe6e6"; - } - case LogLevel.Fatal: { - return "#ff3333"; - } - default: { - return "white"; - } - } - } -} diff --git a/src/Moryx.CommandCenter.Web/src/log/images/Debug.png b/src/Moryx.CommandCenter.Web/src/log/images/Debug.png deleted file mode 100644 index 2cc5dc81b7ad8a6990603f557148261e768c971d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 426 zcmV;b0agBqP)N2bZe?^J zG%heMGBNQWX_Wu~0VhdBK~y+Toszvu!%!5*uLYq?=+I4zAGv)5>jTvH@C{s5e2pZM zuTe0N5E2OJ5F|Ktap)or)m0Y<5eiP%|0%iQ_L2rM@FVA(|M>-OxJ(G~Zc)TEybG|S+-(NdJ7kGL{ljGCr^h8YXW{mZI0RX;c UcM|sEod5s;07*qoM6N<$f_YW0rvLx| diff --git a/src/Moryx.CommandCenter.Web/src/log/images/Error.png b/src/Moryx.CommandCenter.Web/src/log/images/Error.png deleted file mode 100644 index f9476adf7672cf7e4dd89fe5f883afa1dd164bbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^{2!lvI6;>1s;*b z3=DjSK$uZf!>a)(C|TkfQ4*Y=R#Ki=l*&+$n3-3imzP?iV4`QBXJq(MA#*BF(R5E2 z#}Etu?US~H zPR==(<8QjWyoyd~X_Y%jO`SURy@satY3AOeU%1V9SbWr$r4{=5o%`?GXu{0QEX)ui Xdvc29`DgoqPGsbP0l+XkKEr(pl diff --git a/src/Moryx.CommandCenter.Web/src/log/images/Fatal.png b/src/Moryx.CommandCenter.Web/src/log/images/Fatal.png deleted file mode 100644 index 221c3d1655fe3668785d961bebaa759b7785252a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^{2!lvI6;>1s;*b z3=DjSK$uZf!>a)(C|TkfQ4*Y=R#Ki=l*&+$n3-3imzP?iV4`QBXJq(MA#*BF(OORz z#}Etu8#_p>U|9dGmFK*^0+bhN?0RmQ#(@>fXHK6kzG4<* z-@%#`)*mL`r%y~#&Sv|h{a}&;&y|Kbh5=0Ofu5dzk*-hqJQi=-^hs7VHMLUY><BiHC>Bhap>COLHpEhZvy489ZJ6T-G@yGywo<5oy){ diff --git a/src/Moryx.CommandCenter.Web/src/log/images/Info.png b/src/Moryx.CommandCenter.Web/src/log/images/Info.png deleted file mode 100644 index 573e36ed0ca0105cf9b83ad6ff3e4dd3bab13576..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^{20#LT=By}Z;C1rt33 zJtM=93Yk-Zib_3Q978PplYjjGZ+}x_w*s@XlEC)l$X=7soCDMk-IPJ zSm#D=KKZkI_UuvMv|#%@QS^X;Ktjf}X%jWuGg%T9_A}{ua%gL5p442u;tpeGNLbju z#Y@)c^oJ^%@v!))ElVr(^E>z7x6y=|nOT_OOub`&*NUT0fev8sboFyt=akR{07c+Z AyZ`_I diff --git a/src/Moryx.CommandCenter.Web/src/log/images/Trace.png b/src/Moryx.CommandCenter.Web/src/log/images/Trace.png deleted file mode 100644 index d3a1cc8f4f665aa8da3f354df026439f2ff32787..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^{2!lvI6;>1s;*b z3=DjSK$uZf!>a)(C|TkfQ4*Y=R#Ki=l*&+$n3-3imzP?iV4`QBXJq(MA#*BFQG=(8 zV~B-+@{j-j?Qcr7ZcaYlx1OC(=0`t&{>@EK6~oTn5J^64@Ks~hMZ;Z5B|V`6ZSJPY zNo$%6Y%EMZfBM7}YJ9+OM*kWKS>^0$(@%)$&G-Zb3Q`Rv3FbPI#0tDnm{r-UW|%9&S` diff --git a/src/Moryx.CommandCenter.Web/src/log/images/Warning.png b/src/Moryx.CommandCenter.Web/src/log/images/Warning.png deleted file mode 100644 index 132bbd283f7ab3b9175ee79fc094804ca3a08ca8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 436 zcmV;l0ZaagP)N2bZe?^J zG%heMGBNQWX_Wu~0WnELK~y+Tm5|L!12GhZ58clbSQICH1nUFT_wY4zFYevw!j-rb z+`AA8y0D#E(F!6u2vwx`Q)ei(>O$hZF^NgJ8wbwh-tV3>lLT68=xgZrbBs-GHR_^d zJ~+ls$eG<7pvAQZMpGshePrm^YYSV#&VYm-!8nXsdl5 z()lwJu-x6p_$}VfoF-+QEo&2XJA>=;}Lcx z9x+nX{8u8Blq3nc;bnTsC~2lzT?xa~uZl)V^JzM(YPFn$N~t_mYECptqNlP@UtDf9 emUNv~0F@tlo?(6j0kc~G0000 { - return { type: UPDATE_LOGGERS, payload: loggers }; -} diff --git a/src/Moryx.CommandCenter.Web/src/log/redux/LogState.ts b/src/Moryx.CommandCenter.Web/src/log/redux/LogState.ts deleted file mode 100644 index 6269e2d95..000000000 --- a/src/Moryx.CommandCenter.Web/src/log/redux/LogState.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2020, Phoenix Contact GmbH & Co. KG - * Licensed under the Apache License, Version 2.0 -*/ -require("../../types/constants"); -import { ActionType } from "../../common/redux/Types"; -import LogRestClient from "../api/LogRestClient"; -import LoggerModel from "../models/LoggerModel"; -import { UPDATE_LOGGERS } from "./LogActions"; - -export interface LogState { - RestClient: LogRestClient; - Loggers: LoggerModel[]; -} - -export const initialLogState: LogState = { - Loggers: [], - RestClient: new LogRestClient(BASE_URL), -}; - -export function getLogReducer(state: LogState = initialLogState, action: ActionType<{}>): LogState { - switch (action.type) { - case UPDATE_LOGGERS: { - return { ...state, Loggers: action.payload as LoggerModel[] }; - } - } - return state; -} 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 62fcdca20..008a96ed4 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/BooleanEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/BooleanEditor.tsx @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0 */ -import "bootstrap-toggle/css/bootstrap2-toggle.css"; +import "bootstrap5-toggle/css/bootstrap5-toggle.css"; import * as React from "react"; import BootstrapToggle from "react-bootstrap-toggle"; import { Input } from "reactstrap"; @@ -13,15 +13,15 @@ import InputEditorBase, { InputEditorBasePropModel } from "./InputEditorBase"; export default class ByteEditor extends InputEditorBase { constructor(props: InputEditorBasePropModel) { super(props); - this.state = { }; + this.state = {}; } public render(): React.ReactNode { return ( ) => this.onToggle(e)} - height="35px" /> + disabled={this.props.Entry.value.isReadOnly || this.props.IsReadOnly} + onClick={(e: React.MouseEvent) => this.onToggle(e)} + height="35px" /> ); } 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 33bd853a1..04d76e007 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollapsibleEntryEditorBase.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollapsibleEntryEditorBase.tsx @@ -7,7 +7,7 @@ import * as React from "react"; import { Collapse } from "reactstrap"; import Entry from "../../models/Entry"; -export interface CollapsibleEntryEditorBasePropModel { +export interface CollapsibleEntryEditorBasePropModel extends React.PropsWithChildren { Entry: Entry; IsExpanded: boolean; IsReadOnly: boolean; 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 f3737f0c0..651d56cc8 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NavigableConfigEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/NavigableConfigEditor.tsx @@ -4,7 +4,7 @@ */ import { Action, History, Location, UnregisterCallback } from "history"; -import * as qs from "query-string"; +import queryString from "query-string"; import * as React from "react"; import { Button, ButtonGroup, Col, Container, Row } from "reactstrap"; import Entry from "../../models/Entry"; @@ -42,7 +42,7 @@ export default class NavigableConfigEditor extends React.ComponentValue
+ Entries={this.state.Entries} + Root={this.props.Root} + navigateToEntry={this.navigateToEntry.bind(this)} + IsReadOnly={this.props.IsReadOnly} />
); diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/Module.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/Module.tsx index e0273dab5..21815347b 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/container/Module.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/container/Module.tsx @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiCheck, mdiDatabase, mdiHexagon, mdiHexagonMultiple, mdiPlay, mdiRestart, mdiStop} from "@mdi/js"; +import { mdiCheck, mdiDatabase, mdiHexagon, mdiHexagonMultiple, mdiPlay, mdiRestart, mdiStop } from "@mdi/js"; import Icon from "@mdi/react"; import * as React from "react"; import { connect } from "react-redux"; @@ -53,8 +53,8 @@ class Module extends React.Component +
Type @@ -182,10 +182,10 @@ class Module extends React.Component { this.props.Module.dependencies.map((module, idx) => - - {module.name} - - ) + + {module.name} + + ) } @@ -200,7 +200,7 @@ class Module extends React.ComponentStart behaviour: ) => this.onStartBehaviourChange(e)}> + onChange={(e: React.FormEvent) => this.onStartBehaviourChange(e)}> @@ -211,7 +211,7 @@ class Module extends React.ComponentFailure behaviour: ) => this.onFailureBehaviourChange(e)}> + onChange={(e: React.FormEvent) => this.onFailureBehaviourChange(e)}> @@ -235,20 +235,20 @@ class Module extends React.Component - { - this.props.Module.notifications.map((notification, idx) => - ) => this.openNotificationDetailsDialog(e, notification)}> - {notification.exception != null ? notification.exception.exceptionTypeName : "-"} - {notification.message} - - - {Serverity[notification.severity]} - - - , - ) - } + { + this.props.Module.notifications.map((notification, idx) => + ) => this.openNotificationDetailsDialog(e, notification)}> + {notification.exception != null ? notification.exception.exceptionTypeName : "-"} + {notification.message} + + + {Serverity[notification.severity]} + + + , + ) + } )} @@ -262,9 +262,9 @@ class Module extends React.Component - + - {this.state.SelectedNotification.message} + {this.state.SelectedNotification.message} @@ -285,14 +285,14 @@ class Module extends React.Component - { this.state.SelectedNotification.exception.innerException == null ? ( + {this.state.SelectedNotification.exception.innerException == null ? ( No inner exception found. ) : ( Inner exception )} - { this.state.SelectedNotification.exception.innerException != null && + {this.state.SelectedNotification.exception.innerException != null && {Module.preRenderInnerException(this.state.SelectedNotification.exception.innerException)} diff --git a/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConfiguration.tsx b/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConfiguration.tsx index e8abf6177..10582bc5f 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConfiguration.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/container/ModuleConfiguration.tsx @@ -3,13 +3,12 @@ * Licensed under the Apache License, Version 2.0 */ -import { mdiCogs, mdiContentSave, mdiHexagon, mdiSync, mdiUndo} from "@mdi/js"; +import { mdiCogs, mdiContentSave, mdiHexagon, mdiSync, mdiUndo } from "@mdi/js"; import Icon from "@mdi/react"; import * as React from "react"; -import NotificationSystem = require("react-notification-system"); import { RouteComponentProps, withRouter } from "react-router-dom"; -import { Button, ButtonGroup, Card, CardBody, CardHeader, Col, Container, ListGroup, Row } from "reactstrap"; -import ListGroupItem from "reactstrap/lib/ListGroupItem"; +import { toast, ToastContainer } from "react-toastify"; +import { Button, ButtonGroup, Card, CardBody, CardHeader, Col, Container, ListGroup, ListGroupItem, Row } from "reactstrap"; import ModuleHeader from "../../common/components/ModuleHeader"; import ModulesRestClient from "../api/ModulesRestClient"; import NavigableConfigEditor from "../components/ConfigEditor/NavigableConfigEditor"; @@ -20,7 +19,6 @@ import Entry from "../models/Entry"; interface ModuleConfigurationPropModel { RestClient?: ModulesRestClient; ModuleName: string; - NotificationSystem: NotificationSystem; } interface ModuleConfigurationStateModel { @@ -52,33 +50,33 @@ class ModuleConfiguration extends React.Component { return this.props.RestClient.moduleConfig(this.props.ModuleName) - .then((data) => { - Config.patchConfig(data); - this.setState( - { - ModuleConfig: data, - ParentEntry: data.root, - CurrentSubEntries: data.root.subEntries, - ConfigIsLoading: false, - }); - }); + .then((data) => { + Config.patchConfig(data); + this.setState( + { + ModuleConfig: data, + ParentEntry: data.root, + CurrentSubEntries: data.root.subEntries, + ConfigIsLoading: false, + }); + }); } public onApply(): void { this.props.RestClient.saveModuleConfig(this.props.ModuleName, { Config: this.state.ModuleConfig, UpdateMode: ConfigUpdateMode.SaveAndReincarnate }) - .then((result) => this.props.NotificationSystem.addNotification({ title: "Saved", message: "Configuration was saved successfully. Module is restarting...", level: "success", autoDismiss: 5 })); + .then((result) => toast.success("Configuration was saved successfully. Module is restarting...", { autoClose: 5000 })); } public onSave(): void { this.props.RestClient.saveModuleConfig(this.props.ModuleName, { Config: this.state.ModuleConfig, UpdateMode: ConfigUpdateMode.OnlySave }) - .then((result) => this.props.NotificationSystem.addNotification({ title: "Saved", message: "Configuration was saved successfully", level: "success", autoDismiss: 5 })); + .then((result) => toast.success("Configuration was saved successfully", { autoClose: 5000 })); } public onRevert(): void { this.props.history.push("?"); this.setState({ ConfigIsLoading: true }); this.loadConfig() - .then((result) => this.props.NotificationSystem.addNotification({ title: "Reverted", message: "Configuration was reverted", level: "success", autoDismiss: 3 })); + .then((result) => toast.success("Configuration was reverted", { autoClose: 3000 })); } public render(): React.ReactNode { @@ -98,11 +96,11 @@ class ModuleConfiguration extends React.ComponentLoading config ... } + Entries={this.state.CurrentSubEntries} + Root={this.state.ModuleConfig.root} + IsReadOnly={false} + History={this.props.history} + Location={this.props.location} />
); return ( -
+
- { this.props.MenuItem.Content === undefined && + {this.props.MenuItem.Content === undefined && defaultContent } - { this.props.MenuItem.Content !== undefined && + {this.props.MenuItem.Content !== undefined && this.props.MenuItem.Content }
) => this.handleMenuItemClick(e)}> - { hasSubItems && - + {hasSubItems && + }
diff --git a/src/Moryx.CommandCenter.Web/src/common/components/ModuleHeader.tsx b/src/Moryx.CommandCenter.Web/src/common/components/ModuleHeader.tsx index a69e0aaff..3f858560e 100644 --- a/src/Moryx.CommandCenter.Web/src/common/components/ModuleHeader.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/components/ModuleHeader.tsx @@ -48,7 +48,7 @@ export class ModuleHeader extends React.Component
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 36/82] 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 37/82] 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 38/82] 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 39/82] 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 From 1704101b5f2429f240ce0de7d8bb4c0814ac5dcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 13:09:20 +0000 Subject: [PATCH 40/82] Bump react from 18.2.0 to 18.3.1 in /src/Moryx.CommandCenter.Web Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 18.2.0 to 18.3.1. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v18.3.1/packages/react) --- updated-dependencies: - dependency-name: react dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/Moryx.CommandCenter.Web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index e5441ec98..4dc454a9d 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -24,7 +24,7 @@ "moment": "^2.30.1", "path-scurry": "^1.10.2", "query-string": "^9.0.0", - "react": "18.2.0", + "react": "18.3.1", "react-dom": "18.2.0", "react-redux": "^9.1.0", "react-router": "^6.22.0", From 0b520d278b0fe0dd551dcfb433a400648c6b576c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 13:11:20 +0000 Subject: [PATCH 41/82] Bump style-loader from 3.3.4 to 4.0.0 in /src/Moryx.CommandCenter.Web Bumps [style-loader](https://github.com/webpack-contrib/style-loader) from 3.3.4 to 4.0.0. - [Release notes](https://github.com/webpack-contrib/style-loader/releases) - [Changelog](https://github.com/webpack-contrib/style-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/style-loader/compare/v3.3.4...v4.0.0) --- updated-dependencies: - dependency-name: style-loader dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- src/Moryx.CommandCenter.Web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index e5441ec98..71ec2a04b 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -45,7 +45,7 @@ "sass": "^1.72.0", "sass-loader": "^14.1.0", "source-map-loader": "^5.0.0", - "style-loader": "^3.3.4", + "style-loader": "^4.0.0", "tslint": "^6.1.3", "tslint-loader": "^3.5.4", "tslint-react": "^5.0.0", From 4cbf1b010ec21576e82a99158e7f34d9e93fdd75 Mon Sep 17 00:00:00 2001 From: Sascha Priefer Date: Tue, 30 Apr 2024 13:05:55 +0200 Subject: [PATCH 42/82] Change order of activation of the notification facade. Facade must be ready before resources are starting to allow publish notification during OnStart(). --- .../ModuleController/ModuleController.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Moryx.Resources.Management/ModuleController/ModuleController.cs b/src/Moryx.Resources.Management/ModuleController/ModuleController.cs index cbdb687bd..7f33b4777 100644 --- a/src/Moryx.Resources.Management/ModuleController/ModuleController.cs +++ b/src/Moryx.Resources.Management/ModuleController/ModuleController.cs @@ -68,6 +68,8 @@ protected override void OnInitialize() ///
protected override void OnStart() { + ActivateFacade(_notificationSourceFacade); + // Start type controller for resource and proxy creation Container.Resolve().Start(); @@ -80,7 +82,6 @@ protected override void OnStart() // Activate external facade to register events ActivateFacade(_resourceTypeTreeFacade); - ActivateFacade(_notificationSourceFacade); ActivateFacade(_resourceManagementFacade); } @@ -91,11 +92,12 @@ protected override void OnStart() protected override void OnStop() { // Tear down facades - DeactivateFacade(_notificationSourceFacade); DeactivateFacade(_resourceManagementFacade); DeactivateFacade(_resourceTypeTreeFacade); var resourceManager = Container.Resolve(); resourceManager.Stop(); + + DeactivateFacade(_notificationSourceFacade); } private readonly ResourceManagementFacade _resourceManagementFacade = new ResourceManagementFacade(); From deedfb17a536e45b75b69b96aa2e10acb5552379 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Wed, 8 May 2024 13:24:57 +0200 Subject: [PATCH 43/82] Bump version to 8.0.4 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 215aacb45..50c496d20 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.3 +8.0.4 From 2bd3e054cd16861d87eb3e3f4ee3903cee528b3f Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Thu, 16 May 2024 09:50:55 +0200 Subject: [PATCH 44/82] Use renamed `IsPackable` flag instead of `CreatePackage` This fixes, among other things, that the MORYX logo gets included in packages. --- Directory.Build.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 1fb1912e2..d12d3f6bd 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,7 +1,7 @@ - + 8.0.0 @@ -10,8 +10,8 @@ latest - - + + From 25e2bc768ccf763e0205baf9850cfe6f5623184d Mon Sep 17 00:00:00 2001 From: Dennis Beuchler Date: Thu, 16 May 2024 11:25:12 +0200 Subject: [PATCH 45/82] Fixed TcpKeeepAliveRetryCount for older windows systens --- .../Sockets/Client/TcpClientConnection.cs | 4 ++-- .../Communication/Sockets/TcpTransmission.cs | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Moryx/Communication/Sockets/Client/TcpClientConnection.cs b/src/Moryx/Communication/Sockets/Client/TcpClientConnection.cs index 5bc2f5802..bd2726185 100644 --- a/src/Moryx/Communication/Sockets/Client/TcpClientConnection.cs +++ b/src/Moryx/Communication/Sockets/Client/TcpClientConnection.cs @@ -176,7 +176,7 @@ internal void Connected() { _transmission = new TcpTransmission(_tcpClient, _validator.Interpreter, Logger.GetChild(string.Empty, typeof(TcpTransmission))); _transmission.Disconnected += ConnectionClosed; - _transmission.ExceptionOccured += OnTransmissionException; + _transmission.ExceptionOccurred += OnTransmissionException; _transmission.Received += MessageReceived; _transmission.StartReading(); @@ -205,7 +205,7 @@ internal void Disconnect() // First close the connection and then unregister events transmission.Disconnect(); - transmission.ExceptionOccured -= OnTransmissionException; + transmission.ExceptionOccurred -= OnTransmissionException; transmission.Received -= MessageReceived; transmission.Disconnected -= ConnectionClosed; diff --git a/src/Moryx/Communication/Sockets/TcpTransmission.cs b/src/Moryx/Communication/Sockets/TcpTransmission.cs index 8a59633a8..935162f48 100644 --- a/src/Moryx/Communication/Sockets/TcpTransmission.cs +++ b/src/Moryx/Communication/Sockets/TcpTransmission.cs @@ -31,7 +31,7 @@ internal class TcpTransmission : IBinaryTransmission, IDisposable /// /// Callback to forward transmission exceptions to connection /// - public event EventHandler ExceptionOccured; + public event EventHandler ExceptionOccurred; /// /// Initialize TcpTransmission @@ -53,10 +53,10 @@ public TcpTransmission(TcpClient client, IMessageInterpreter interpreter, ILogge private void RaiseException(Exception ex) { - if (ExceptionOccured == null) + if (ExceptionOccurred == null) _logger.Log(LogLevel.Error, ex, "TcpTransmission encountered an error"); else - ExceptionOccured(this, ex); + ExceptionOccurred(this, ex); } private void RaiseDisconnected() @@ -81,7 +81,19 @@ public void ConfigureKeepAlive(int interval, int timeout) // Configure socket using net6 keep alive configuration socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, interval); socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, timeout); - socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 2); + + try + { + // Try to set the TcpKeeepAliveRetryCount, it is not supported on all windows systems + // https://learn.microsoft.com/en-us/windows/win32/winsock/ipproto-tcp-socket-options#windows-support-for-ipproto_tcp-options + socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 2); + + } + catch(Exception ex) + { + _logger.Log(LogLevel.Warning, ex, "TcpKeepAliveRetryCount could not be applied!"); + } + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); #else // Keep alive only supported for windows under netstandard2.0 From 3f22821169fe98f1b6fc2811e9b0c039fe45fc1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 06:37:45 +0000 Subject: [PATCH 46/82] Bump rimraf from 5.0.5 to 5.0.7 in /src/Moryx.CommandCenter.Web Bumps [rimraf](https://github.com/isaacs/rimraf) from 5.0.5 to 5.0.7. - [Changelog](https://github.com/isaacs/rimraf/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/rimraf/compare/v5.0.5...v5.0.7) --- updated-dependencies: - dependency-name: rimraf dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/Moryx.CommandCenter.Web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index e5441ec98..420245622 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", - "rimraf": "5.0.5", + "rimraf": "5.0.7", "sass": "^1.72.0", "sass-loader": "^14.1.0", "source-map-loader": "^5.0.0", From 36ca13b14b617e7c95622ed7e53d85bdae7b3514 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Wed, 22 May 2024 12:05:46 +0200 Subject: [PATCH 47/82] Prevent hidden config values from being rendered The contents of `CollectionEditor` and `ClassEditor` shouldn't get rendered unless they are visible, i.e. expanded. This prevents the UI from loading for a long time with huge configurations. --- .../src/modules/components/ConfigEditor/ClassEditor.tsx | 9 ++++++--- .../modules/components/ConfigEditor/CollectionEditor.tsx | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) 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 2b0f48c2b..f74f3c8ef 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ClassEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/ClassEditor.tsx @@ -30,9 +30,12 @@ export default class ClassEditor extends CollapsibleEntryEditorBase - - {this.preRenderConfigEditor()} - + { + this.props.IsExpanded && + + {this.preRenderConfigEditor()} + + } ); 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 afc410382..dc212eca4 100644 --- a/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollectionEditor.tsx +++ b/src/Moryx.CommandCenter.Web/src/modules/components/ConfigEditor/CollectionEditor.tsx @@ -128,7 +128,7 @@ export default class CollectionEditor extends CollapsibleEntryEditorBase - { + { this.props.IsExpanded && this.props.Entry.subEntries.map((entry, idx) => { if (Entry.isClassOrCollection(entry)) { return ( From ca31a66ec46e0201b37a021577c128df15757c81 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Wed, 19 Jun 2024 07:12:28 +0200 Subject: [PATCH 48/82] Add integration test tools for MORYX modules --- MORYX-Framework.sln | 13 +- docs/tutorials/HowToTestAModule.md | 45 ++++++ .../Moryx.TestTools.IntegrationTest.csproj | 20 +++ .../MoryxTestEnvironment.cs | 130 ++++++++++++++++++ 4 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 docs/tutorials/HowToTestAModule.md create mode 100644 src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj create mode 100644 src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs diff --git a/MORYX-Framework.sln b/MORYX-Framework.sln index f3ba24f65..4b91aaf84 100644 --- a/MORYX-Framework.sln +++ b/MORYX-Framework.sln @@ -116,7 +116,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Resources.Management. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.Tests", "src\Tests\Moryx.Runtime.Endpoints.Tests\Moryx.Runtime.Endpoints.Tests.csproj", "{7792C4E0-6D07-42C9-AC29-BAB76836FC11}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -296,6 +298,10 @@ Global {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.Build.0 = Release|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,10 +346,11 @@ Global {FEB3BA44-2CD9-445A-ABF2-C92378C443F7} = {0A466330-6ED6-4861-9C94-31B1949CDDB9} {7792C4E0-6D07-42C9-AC29-BAB76836FC11} = {0A466330-6ED6-4861-9C94-31B1949CDDB9} {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8} = {8517D209-5BC1-47BD-A7C7-9CF9ADD9F5B6} + {C949164C-0345-4893-9E4C-A79BC1F93F85} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243} - RESX_TaskErrorCategory = Message RESX_ShowErrorsInErrorList = True + RESX_TaskErrorCategory = Message + SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243} EndGlobalSection EndGlobal diff --git a/docs/tutorials/HowToTestAModule.md b/docs/tutorials/HowToTestAModule.md new file mode 100644 index 000000000..039be9c72 --- /dev/null +++ b/docs/tutorials/HowToTestAModule.md @@ -0,0 +1,45 @@ +# Setup a test environment for integration tests of a module + +In order to test a module in its lifecycle with its respective facade we offer the `Moryx.TestTools.IntegrationTest`. +The package brings a `MoryxTestEnvironment`. +With this class you can first create mocks for all module facades your module dependents on using the static `CreateModuleMock` method. +Afterwards you can create the environment using an implementation of the `ServerModuleFacadeControllerBase`, an instance of the `ConfigBase` and the set of dependency mocks. +The first two parameters are usually your `ModuleController` and your `ModuleConfig`. +The following example shows a setup for the `IShiftManagement` facade interface. The module depends on the `IResourceManagement` and `IOperatorManagement` facades. + +```csharp +private ModuleConfig _config; +private Mock _resourceManagementMock; +private Mock _operatorManagementMock; +private MoryxTestEnvironment _env; + +[SetUp] +public void SetUp() +{ + ReflectionTool.TestMode = true; + _config = new(); + _resourceManagementMock = MoryxTestEnvironment.CreateModuleMock(); + _operatorManagementMock = MoryxTestEnvironment.CreateModuleMock(); + _env = new MoryxTestEnvironment(typeof(ModuleController), + new Mock[] { _resourceManagementMock, _operatorManagementMock }, _config); +} +``` + +Using the created environment you can start and stop the module as you please. +You can also retrieve the facade of the module to test all the functionalities the running module should provide. + +```csharp +[Test] +public void Start_WhenModuleIsStopped_StartsModule() +{ + // Arrange + var facade = _env.GetTestModule(); + + // Act + var module = _env.StartTestModule(); + var module = _env.StopTestModule(); + + // Assert + Assert.That(module.State, Is.EqualTo(ServerModuleState.Stopped)); +} +``` \ No newline at end of file diff --git a/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj b/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj new file mode 100644 index 000000000..82a71b2cd --- /dev/null +++ b/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + Library with helper classes for integration tests. + true + MORYX;Tests;IntegrationTest + true + + + + + + + + + + + + diff --git a/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs b/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs new file mode 100644 index 000000000..97ff43398 --- /dev/null +++ b/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs @@ -0,0 +1,130 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; +using Moq; +using Moryx.Configuration; +using Moryx.Model.InMemory; +using Moryx.Model; +using Moryx.Runtime.Container; +using Moryx.Runtime.Kernel; +using Moryx.Runtime.Modules; +using Moryx.TestTools.UnitTest; +using Moryx.Threading; +using System; +using System.Linq; +using Moryx.Tools; +using System.Collections.Generic; + +namespace Moryx.Shifts.Management.IntegrationTests +{ + /// + /// A test environment for MORYX modules to test the module lifecycle as well as its + /// facade and component orchestration. The environment must be filled with mocked + /// dependencies. + /// + /// Type of the facade to be tested. + public class MoryxTestEnvironment where T : class + { + public IServiceProvider Services { get; private set; } + + /// + /// Creates an for integration tests of moryx. We prepare the + /// service collection to hold all kernel components (a mocked IConfigManager providing only the , + /// , an , a and the + /// ). Additionally all provided mocks are registered as moryx modules. + /// + /// Type of the ModuleController of the module to be tested + /// An enumeration of mocks for all dependencies of the module to be tested. + /// We recommend using the method to properly create the mocks. + /// The config for the module to be tested. + /// Throw if is not a server module + public MoryxTestEnvironment(Type serverModuleType, IEnumerable dependencyMocks, ConfigBase config) + { + if (!serverModuleType.IsAssignableTo(typeof(IServerModule))) + throw new ArgumentException("Provided parameter is no server module", nameof(serverModuleType)); + + var dependencyTypes = serverModuleType.GetProperties() + .Where(p => p.GetCustomAttribute() is not null) + .Select(p => p.PropertyType); + + var services = new ServiceCollection(); + foreach (var type in dependencyTypes) + { + var mock = dependencyMocks.SingleOrDefault(m => type.IsAssignableFrom(m.Object.GetType())) ?? + throw new ArgumentException($"Missing {nameof(Mock)} for dependency of type {type} of facade type {serverModuleType}", nameof(dependencyMocks)); + services.AddSingleton(type, mock.Object); + services.AddSingleton(typeof(IServerModule), mock.Object); + } + + services.AddMoryxKernel(); + var configManagerMock = new Mock(); + configManagerMock.Setup(c => c.GetConfiguration(config.GetType(), It.IsAny(), false)).Returns(config); + services.AddSingleton(configManagerMock.Object); + + var parallelOpsDescriptor = services.Single(d => d.ServiceType == typeof(IParallelOperations)); + services.Remove(parallelOpsDescriptor); + services.AddTransient(); + services.AddSingleton(new InMemoryDbContextManager(Guid.NewGuid().ToString())); + services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new Mock>().Object); + services.AddMoryxModules(); + + Services = services.BuildServiceProvider(); + _ = Services.GetRequiredService(); + } + + /// + /// Creates a mock of a server module with a facade interface of type . + /// The mock can be used in setting up a service collection for test purposes. + /// + /// Type of the facade interface + /// The mock of the + public static Mock CreateModuleMock() where FacadeType : class + { + var mock = new Mock(); + var moduleMock = mock.As(); + moduleMock.SetupGet(m => m.State).Returns(ServerModuleState.Running); + var containerMock = moduleMock.As>(); + containerMock.SetupGet(x => x.Facade).Returns(mock.Object); + return mock; + } + + /// + /// Initializes and starts the module with the facade interface of type + /// . + /// + /// The started module. + public IServerModule StartTestModule() + { + var module = Services.GetServices() + .Single(s => s.GetType().IsAssignableTo(typeof(IFacadeContainer))); + + module.Initialize(); + var containerHost = module as IContainerHost; + containerHost.Container.Register(nameof(NotSoParallelOps), Container.LifeCycle.Singleton); + if (!containerHost.Strategies.Any(s => s.Value == nameof(NotSoParallelOps))) + containerHost.Strategies.Add(typeof(IParallelOperations), nameof(NotSoParallelOps)); + + module.Start(); + return module; + } + + /// + /// Stops the module with the facade interface of type . + /// + /// The stopped module. + public IServerModule StopTestModule() + { + var module = Services.GetServices() + .Single(s => s.GetType().IsAssignableTo(typeof(IFacadeContainer))); + module.Stop(); + + return module; + } + + /// + /// Returns the service for the facade of type to be tested. + /// + public T GetTestModule() => Services.GetRequiredService(); + } +} \ No newline at end of file From 7684fcbb74e4b1eb02adeeab507f8e057ceb7774 Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Thu, 4 Jul 2024 12:53:21 +0200 Subject: [PATCH 49/82] Bump version to 8.0.5 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 50c496d20..904be6d4e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.4 +8.0.5 From 53a35c54e6ad38516e1ee3c32353d930382deea7 Mon Sep 17 00:00:00 2001 From: Matho Camara Date: Thu, 18 Jul 2024 14:49:17 +0200 Subject: [PATCH 50/82] load type from database before raising TypeChange event --- .../ProductConverter.cs | 7 ++++++- .../Implementation/ProductManager.cs | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs index 287309c43..18bdc0475 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs @@ -270,16 +270,21 @@ private void UpdateCollection(IList value, IEnumerable parts) continue; var match = unused.Find(r => r.Id == partModel?.Id); + // new partlink if (match == null) { match = (IProductPartLink)Activator.CreateInstance(elemType); + match.Product = _productManagement.LoadType(partModel.Product.Id); value.Add(match); } + //modified reference + else if (match.Product.Id != partModel.Product.Id) + match.Product = _productManagement.LoadType(partModel.Product.Id); else + //part removed unused.Remove(match); EntryConvert.UpdateInstance(match, partModel.Properties); - match.Product = _productManagement.LoadType(partModel.Product.Id); } // Clear all values no longer present in the model diff --git a/src/Moryx.Products.Management/Implementation/ProductManager.cs b/src/Moryx.Products.Management/Implementation/ProductManager.cs index 68c4cf8ad..e20f1cdb9 100644 --- a/src/Moryx.Products.Management/Implementation/ProductManager.cs +++ b/src/Moryx.Products.Management/Implementation/ProductManager.cs @@ -79,7 +79,10 @@ public IProductType LoadType(ProductIdentity identity) public long SaveType(IProductType modifiedInstance) { var saved = Storage.SaveType(modifiedInstance); - RaiseProductChanged(modifiedInstance); + //reload the object for correct references + var loadedType = Storage.LoadType(saved); + RaiseProductChanged(loadedType); + return saved; } From acfd679a2c32912deec3c28aa5811f3bfd27b2f3 Mon Sep 17 00:00:00 2001 From: Matho Camara Date: Mon, 22 Jul 2024 11:18:50 +0200 Subject: [PATCH 51/82] update `ProductConvertion` UnitTest to call the `LoadType` method once --- .../Moryx.Products.Management.Tests/ProductConverterTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs b/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs index a79dd1eae..403e99218 100644 --- a/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs +++ b/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs @@ -129,10 +129,7 @@ public void ForwardBackwardProductConversionWithoutInformationLoss(DummyProductT // - If there are ProductPartLinks the ProductManagement should be called var targetDummyTypeWithParts = recoveredOriginal as DummyProductTypeWithParts; if (targetDummyTypeWithParts?.ProductPartLink?.Product is not null) - { _productManagerMock.Verify(pm => pm.LoadType(targetDummyTypeWithParts.ProductPartLink.Product.Id)); - _productManagerMock.Verify(pm => pm.LoadType(targetDummyTypeWithParts.ProductPartLinkEnumerable.First().Product.Id)); - } } private static bool HasChangedProperties(object A, object B) From 3eb9907b07c9eb901ca5e440f94ef47f6835351f Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Mon, 29 Jul 2024 14:06:12 +0200 Subject: [PATCH 52/82] Remove class generic for simplified use --- docs/tutorials/HowToTestAModule.md | 8 ++++---- .../MoryxTestEnvironment.cs | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/tutorials/HowToTestAModule.md b/docs/tutorials/HowToTestAModule.md index 039be9c72..2796eecf6 100644 --- a/docs/tutorials/HowToTestAModule.md +++ b/docs/tutorials/HowToTestAModule.md @@ -11,16 +11,16 @@ The following example shows a setup for the `IShiftManagement` facade interface. private ModuleConfig _config; private Mock _resourceManagementMock; private Mock _operatorManagementMock; -private MoryxTestEnvironment _env; +private MoryxTestEnvironment _env; [SetUp] public void SetUp() { ReflectionTool.TestMode = true; _config = new(); - _resourceManagementMock = MoryxTestEnvironment.CreateModuleMock(); - _operatorManagementMock = MoryxTestEnvironment.CreateModuleMock(); - _env = new MoryxTestEnvironment(typeof(ModuleController), + _resourceManagementMock = MoryxTestEnvironment.CreateModuleMock(); + _operatorManagementMock = MoryxTestEnvironment.CreateModuleMock(); + _env = new MoryxTestEnvironment(typeof(ModuleController), new Mock[] { _resourceManagementMock, _operatorManagementMock }, _config); } ``` diff --git a/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs b/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs index 97ff43398..b794b7761 100644 --- a/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs +++ b/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs @@ -23,8 +23,10 @@ namespace Moryx.Shifts.Management.IntegrationTests /// dependencies. /// /// Type of the facade to be tested. - public class MoryxTestEnvironment where T : class + public class MoryxTestEnvironment { + private readonly Type _moduleType; + public IServiceProvider Services { get; private set; } /// @@ -40,6 +42,8 @@ public class MoryxTestEnvironment where T : class /// Throw if is not a server module public MoryxTestEnvironment(Type serverModuleType, IEnumerable dependencyMocks, ConfigBase config) { + _moduleType = serverModuleType; + if (!serverModuleType.IsAssignableTo(typeof(IServerModule))) throw new ArgumentException("Provided parameter is no server module", nameof(serverModuleType)); @@ -96,8 +100,7 @@ public static Mock CreateModuleMock() where FacadeType : /// The started module. public IServerModule StartTestModule() { - var module = Services.GetServices() - .Single(s => s.GetType().IsAssignableTo(typeof(IFacadeContainer))); + var module = (IServerModule)Services.GetService(_moduleType); module.Initialize(); var containerHost = module as IContainerHost; @@ -115,8 +118,7 @@ public IServerModule StartTestModule() /// The stopped module. public IServerModule StopTestModule() { - var module = Services.GetServices() - .Single(s => s.GetType().IsAssignableTo(typeof(IFacadeContainer))); + var module = (IServerModule)Services.GetService(_moduleType); module.Stop(); return module; @@ -125,6 +127,6 @@ public IServerModule StopTestModule() /// /// Returns the service for the facade of type to be tested. /// - public T GetTestModule() => Services.GetRequiredService(); + public TModule GetTestModule() => Services.GetRequiredService(); } } \ No newline at end of file From 5f0457ec6c571a3d2e1e1a7c9f77ee4ed95797bb Mon Sep 17 00:00:00 2001 From: Matho Camara Date: Tue, 23 Jul 2024 13:57:18 +0200 Subject: [PATCH 53/82] update variable names for readability --- .../ProductConverter.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs index 18bdc0475..545315451 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs @@ -258,7 +258,7 @@ public IProductType ConvertProductBack(ProductModel source, ProductType converte private void UpdateCollection(IList value, IEnumerable parts) { // Track which part links are still represented by the models - var unused = new List(value.OfType()); + var oldParts = new List(value.OfType()); // Iterate over the part models // Create or update the part links var elemType = value.GetType().GetInterfaces() @@ -269,27 +269,27 @@ private void UpdateCollection(IList value, IEnumerable parts) if (partModel is null) continue; - var match = unused.Find(r => r.Id == partModel?.Id); + var oldPartMatch = oldParts.Find(r => r.Id == partModel.Id); // new partlink - if (match == null) + if (oldPartMatch == null) { - match = (IProductPartLink)Activator.CreateInstance(elemType); - match.Product = _productManagement.LoadType(partModel.Product.Id); - value.Add(match); + oldPartMatch = (IProductPartLink)Activator.CreateInstance(elemType); + oldPartMatch.Product = _productManagement.LoadType(partModel.Product.Id); + value.Add(oldPartMatch); } //modified reference - else if (match.Product.Id != partModel.Product.Id) - match.Product = _productManagement.LoadType(partModel.Product.Id); + else if (oldPartMatch.Product.Id != partModel.Product.Id) + oldPartMatch.Product = _productManagement.LoadType(partModel.Product.Id); else - //part removed - unused.Remove(match); + // existing unchanged partlink: do not delete at the end + oldParts.Remove(oldPartMatch); - EntryConvert.UpdateInstance(match, partModel.Properties); + EntryConvert.UpdateInstance(oldPartMatch, partModel.Properties); } // Clear all values no longer present in the model - foreach (var link in unused) - value.Remove(link); + foreach (var part in oldParts) + value.Remove(part); } private void UpdateReference(IProductPartLink value, PartModel part) From 13362b15f5b7541321c9b5ccb4435affa8a5adb7 Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Mon, 26 Aug 2024 13:51:58 +0200 Subject: [PATCH 54/82] Fix repository validation that failes for inverted type list --- .../Repositories/Proxy/RepositoryProxyBuilder.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Moryx.Model/Repositories/Proxy/RepositoryProxyBuilder.cs b/src/Moryx.Model/Repositories/Proxy/RepositoryProxyBuilder.cs index 900bf853a..be999944d 100644 --- a/src/Moryx.Model/Repositories/Proxy/RepositoryProxyBuilder.cs +++ b/src/Moryx.Model/Repositories/Proxy/RepositoryProxyBuilder.cs @@ -48,7 +48,7 @@ static RepositoryProxyBuilder() public Type Build(Type repoApi) { // Check interface - ValidateRepositoryApi(repoApi, false); + ValidateRepositoryApi(repoApi); var proxyName = CreateProxyName(repoApi); @@ -89,7 +89,7 @@ public Type Build(Type repoApi) public Type Build(Type repoApi, Type repoImpl) { // Check interface - ValidateRepositoryApi(repoApi, false); + ValidateRepositoryApi(repoApi); var proxyName = CreateProxyName(repoImpl); @@ -201,7 +201,7 @@ private static bool IsModificationTracked(Type entityType) return typeof(IModificationTrackedEntity).IsAssignableFrom(entityType); } - private static void ValidateRepositoryApi(Type repoApi, bool additionalApis) + private static void ValidateRepositoryApi(Type repoApi) { if (!repoApi.IsInterface) { @@ -209,11 +209,7 @@ private static void ValidateRepositoryApi(Type repoApi, bool additionalApis) } var repoInterfaces = repoApi.GetInterfaces(); - if (additionalApis && (repoInterfaces.Length > 2 || repoInterfaces.First().GetGenericTypeDefinition() != typeof(IRepository<>))) - { - throw new InvalidOperationException($"'{repoApi.Name}' API does not inherit from IRepository"); - } - else if (repoInterfaces.Length != 2 || repoInterfaces.First().GetGenericTypeDefinition() != typeof(IRepository<>)) + if (!repoInterfaces.Any(rI => rI.IsGenericType && rI.GetGenericTypeDefinition() == typeof(IRepository<>))) { throw new InvalidOperationException($"'{repoApi.Name}' API does not inherit from IRepository"); } From 9e3e2ed0cd4be46160a119ef5b00d9c3046fba50 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Tue, 27 Aug 2024 10:13:43 +0200 Subject: [PATCH 55/82] Bump version to 8.0.6 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 0b2eb36f5..68d92dd66 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.7.2 +8.0.6 From bae19109432ed56fec592d77e900e33ec3b57c99 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Wed, 28 Aug 2024 08:29:15 +0200 Subject: [PATCH 56/82] Bump version to 8.1.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index dc0208aba..8104cabd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.3.1 +8.1.0 From ca09a3d19043315c38e0178d181f84435a536c05 Mon Sep 17 00:00:00 2001 From: Andre Niggemann Date: Thu, 11 Jul 2024 11:14:56 +0200 Subject: [PATCH 57/82] InitialDesign for MAssert and modified Transition Test as an example for the usage. Feel free to break the tests and switch between Assert.That and MAssert.That to see the effects. --- Directory.build.props | 2 +- MORYX-Framework.sln | 7 ++ src/Moryx.TestTools.NUnit/MAssert.cs | 54 +++++++++++++++ .../Moryx.TestTools.NUnit.csproj | 13 ++++ src/Tests/Moryx.Tests/Moryx.Tests.csproj | 1 + .../Moryx.Tests/Workplans/TransitionTests.cs | 65 +++++++++++-------- 6 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 src/Moryx.TestTools.NUnit/MAssert.cs create mode 100644 src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj diff --git a/Directory.build.props b/Directory.build.props index 52d408eb4..c735b57d6 100644 --- a/Directory.build.props +++ b/Directory.build.props @@ -1,6 +1,6 @@ - 9.0 + 10.0 diff --git a/MORYX-Framework.sln b/MORYX-Framework.sln index 4b91aaf84..a62cf04a1 100644 --- a/MORYX-Framework.sln +++ b/MORYX-Framework.sln @@ -118,6 +118,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.NUnit", "src\Moryx.TestTools.NUnit\Moryx.TestTools.NUnit.csproj", "{6FF878E0-AF61-4C3A-9B9C-71C35A949E51}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}" EndProject Global @@ -298,6 +300,10 @@ Global {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.Build.0 = Release|Any CPU + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51}.Release|Any CPU.Build.0 = Release|Any CPU {C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.Build.0 = Debug|Any CPU {C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -346,6 +352,7 @@ Global {FEB3BA44-2CD9-445A-ABF2-C92378C443F7} = {0A466330-6ED6-4861-9C94-31B1949CDDB9} {7792C4E0-6D07-42C9-AC29-BAB76836FC11} = {0A466330-6ED6-4861-9C94-31B1949CDDB9} {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8} = {8517D209-5BC1-47BD-A7C7-9CF9ADD9F5B6} + {6FF878E0-AF61-4C3A-9B9C-71C35A949E51} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F} {C949164C-0345-4893-9E4C-A79BC1F93F85} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/Moryx.TestTools.NUnit/MAssert.cs b/src/Moryx.TestTools.NUnit/MAssert.cs new file mode 100644 index 000000000..8c75378d0 --- /dev/null +++ b/src/Moryx.TestTools.NUnit/MAssert.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using NUnit.Framework.Constraints; +using NUnit.Framework.Internal; +using System; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace Moryx.TestTools.NUnit +{ + public abstract class MAssert + { + public static void That(bool condition, string? message = null, [CallerArgumentExpression(nameof(condition))] string? predicateExpression = null) + { + That(condition, Is.True, message, predicateExpression); + } + public static void That(Func predicate, string? message = null, [CallerArgumentExpression(nameof(predicate))]string? predicateExpression = null) + { + That(predicate, Is.True, message, predicateExpression); + } + + public static void That(T actual, IResolveConstraint constraint, string? message = null, [CallerArgumentExpression(nameof(actual))] string? predicateExpression = null) + { + if (message != null) + { + message = $"{message}\nExpression: {predicateExpression}"; + } + else + { + message = predicateExpression; + } + Assert.That(actual, constraint, message); + } + + public static void That(Func actualExpression, Constraint constraint, string? message = null, [CallerArgumentExpression(nameof(actualExpression))] string? predicateExpression = null) + { + if (message != null) + { + message = $"{message}\nExpression: {predicateExpression}"; + } + else + { + message = predicateExpression; + } + int fails = TestExecutionContext.CurrentContext.CurrentResult.PendingFailures; + T value = default(T)!; + Assert.That(() => value = actualExpression(), new ThrowsNothingConstraint(), $"{message}\nExpected {constraint.Description} and"); + if (TestExecutionContext.CurrentContext.CurrentResult.PendingFailures > fails) return; // TODO: Check if we there could be multithreading issues and whether or not we care + Assert.That(value, constraint, message); + } + } + +} diff --git a/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj b/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj new file mode 100644 index 000000000..06fb36032 --- /dev/null +++ b/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/Tests/Moryx.Tests/Moryx.Tests.csproj b/src/Tests/Moryx.Tests/Moryx.Tests.csproj index 1ff29d6a4..ce8ad8b6e 100644 --- a/src/Tests/Moryx.Tests/Moryx.Tests.csproj +++ b/src/Tests/Moryx.Tests/Moryx.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Tests/Moryx.Tests/Workplans/TransitionTests.cs b/src/Tests/Moryx.Tests/Workplans/TransitionTests.cs index a001cf612..c5852a3ac 100644 --- a/src/Tests/Moryx.Tests/Workplans/TransitionTests.cs +++ b/src/Tests/Moryx.Tests/Workplans/TransitionTests.cs @@ -1,8 +1,10 @@ // Copyright (c) 2023, Phoenix Contact GmbH & Co. KG // Licensed under the Apache License, Version 2.0 +using System; using System.Collections.Generic; using System.Linq; +using Moryx.TestTools.NUnit; using Moryx.Workplans; using Moryx.Workplans.Transitions; using NUnit.Framework; @@ -56,14 +58,15 @@ public void SplitTransition() // Act trans.Initialize(); _inputs[0].Add(_token); - + // Assert - Assert.AreEqual(0, _inputs[0].Tokens.Count()); - Assert.IsTrue(_outputs.All(o => o.Tokens.Count() == 1)); - Assert.IsInstanceOf(_outputs[0].Tokens.First()); - Assert.IsInstanceOf(_outputs[1].Tokens.First()); - Assert.AreEqual(_token, ((SplitToken)_outputs[0].Tokens.First()).Original); - Assert.AreEqual(_token, ((SplitToken)_outputs[1].Tokens.First()).Original); + Assert.Multiple(() => + { + MAssert.That(_inputs[0].Tokens, Is.Empty); + MAssert.That(_outputs.Select(o => o.Tokens), Has.All.Count.EqualTo(1)); + MAssert.That(() => ((SplitToken)_outputs[0].Tokens.First()).Original, Is.EqualTo(_token)); + MAssert.That(() => ((SplitToken)_outputs[1].Tokens.First()).Original, Is.EqualTo(_token)); + }); } [Test] @@ -83,11 +86,13 @@ public void JoinTransition() trans.Initialize(); _inputs[0].Add(split1); _inputs[1].Add(split2); - // Assert - Assert.IsTrue(_inputs.All(i => !i.Tokens.Any())); - Assert.AreEqual(1, _outputs[0].Tokens.Count()); - Assert.AreEqual(_token, _outputs[0].Tokens.First()); + Assert.Multiple(() => + { + MAssert.That(_inputs.All(i => !i.Tokens.Any())); + MAssert.That(_outputs[0].Tokens.Count(), Is.EqualTo(1), "The split token should be joined into one"); + MAssert.That(_outputs[0].Tokens.First(), Is.EqualTo(_token)); + }); } [TestCase(0, Description = "Place only one split token on the first input")] @@ -108,9 +113,12 @@ public void IncompleteJoinTransition(int index) _inputs[index].Add(split); // Assert - Assert.AreEqual(1, _inputs[index].Tokens.Count()); - Assert.AreEqual(0, _inputs[(index + 1) % 2].Tokens.Count()); - Assert.AreEqual(0, _outputs[0].Tokens.Count()); + Assert.Multiple(() => + { + MAssert.That(_inputs[index].Tokens, Has.Count.EqualTo(1)); + MAssert.That(_inputs[(index + 1) % 2].Tokens, Is.Empty); + MAssert.That(_outputs[0].Tokens, Is.Empty); + }); } [Test] @@ -138,10 +146,12 @@ public void SubWorkplanTransition() _inputs[0].Add(_token); // Assert - Assert.AreEqual(0, _inputs[0].Tokens.Count()); - Assert.AreEqual(_token, _outputs[0].Tokens.First()); - Assert.AreEqual(2, triggered.Count); - Assert.IsTrue(triggered.All(t => t is DummyTransition)); + Assert.Multiple(() => { + MAssert.That(_inputs[0].Tokens, Is.Empty); + MAssert.That(() => _outputs[0].Tokens.First(), Is.EqualTo(_token)); + MAssert.That(triggered.Count, Is.EqualTo(2)); + MAssert.That(triggered, Has.All.InstanceOf()); + }); } [Test] @@ -171,14 +181,17 @@ public void SubworkplanPause() trans.Resume(); // Assert - Assert.AreEqual(0, _inputs[0].Tokens.Count()); - Assert.AreEqual(_token, _outputs[0].Tokens.First()); - Assert.AreEqual(1, triggered.Count); - Assert.IsInstanceOf(state); - var snapshot = (WorkplanSnapshot)state; - Assert.AreEqual(1, snapshot.Holders.Length); - var stepId = workplan.Steps.First(s => s is PausableStep).Id; - Assert.AreEqual(stepId, snapshot.Holders[0].HolderId); + Assert.Multiple(() => + { + MAssert.That(_inputs[0].Tokens, Is.Empty); + MAssert.That(_outputs[0].Tokens.First(), Is.EqualTo(_token)); + MAssert.That(triggered, Has.Count.EqualTo(1)); + MAssert.That(state, Is.InstanceOf()); + var snapshot = (WorkplanSnapshot)state; + MAssert.That(snapshot.Holders, Has.Length.EqualTo(1)); + var stepId = workplan.Steps.First(s => s is PausableStep).Id; + MAssert.That(snapshot.Holders[0].HolderId, Is.EqualTo(stepId)); + }); } } } From c7909588fa89bed53f412598574a6f6a3ff7bef5 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Wed, 4 Sep 2024 11:24:59 +0200 Subject: [PATCH 58/82] Port NUnit test tools to release 8 --- src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj b/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj index 06fb36032..7fea020cc 100644 --- a/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj +++ b/src/Moryx.TestTools.NUnit/Moryx.TestTools.NUnit.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable From 4e89aecbb14545d973987830858ded81b4f575e2 Mon Sep 17 00:00:00 2001 From: pbls31 Date: Fri, 13 Sep 2024 10:42:39 +0200 Subject: [PATCH 59/82] Applied changes from latest dev to avoid NullReferenceExceptions when accessing value of property --- .../ValueProvider/ValueProviderExecutor.cs | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Moryx/Configuration/ValueProvider/ValueProviderExecutor.cs b/src/Moryx/Configuration/ValueProvider/ValueProviderExecutor.cs index 088e7dced..98ed1b509 100644 --- a/src/Moryx/Configuration/ValueProvider/ValueProviderExecutor.cs +++ b/src/Moryx/Configuration/ValueProvider/ValueProviderExecutor.cs @@ -36,6 +36,11 @@ public void FillEmpty(object obj) /// public static void Execute(object targetObject, ValueProviderExecutorSettings settings) { + if (targetObject is null) + { + throw new ArgumentNullException(nameof(targetObject)); + } + if (settings.Providers == null) { throw new ArgumentNullException(nameof(settings.Providers)); @@ -55,30 +60,34 @@ private static void Iterate(object target, ValueProviderExecutorSettings setting { foreach (var settingsProvider in settings.Providers) { - if (settingsProvider.Handle(target, property) == ValueProviderResult.Handled) + try { - break; + if (settingsProvider.Handle(target, property) == ValueProviderResult.Handled) + { + break; + } + } + catch (Exception) + { + // TODO: Restrict exception type + // TODO: Consider enabling logging } } var value = property.GetValue(target); - - if (property.PropertyType.IsValueType && !property.PropertyType.IsPrimitive || - property.PropertyType.IsClass && - property.PropertyType != typeof(string) && - !(value is IEnumerable)) + // Iterate each item of an enumerable + if (value is IEnumerable enumerable) { - Iterate(value, settings); - } - - if (value is IEnumerable) - { - var enumerable = value as IEnumerable; foreach (var item in enumerable) { - Iterate(item, settings); + if (item != null) + Iterate(item, settings); } } + else if (value != null && property.PropertyType.IsClass && property.PropertyType != typeof(string)) + { + Iterate(value, settings); + } } } From 626b42b38ac63f2c2ee2ed57107954b67417c224 Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Tue, 17 Sep 2024 15:07:13 +0200 Subject: [PATCH 60/82] Update JSON library for security vulnerability --- Directory.Build.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 801993952..1d84decb3 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -26,7 +26,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From 181526b2161cde23a8885081ca1ac1d333572b8a Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Wed, 18 Sep 2024 11:18:51 +0200 Subject: [PATCH 61/82] Add DefaultValue handling for Instatiate resources --- .../DataMemberAttributeValueProviderFilter.cs | 26 +++++++++++++++ ...bstractionLayer.Resources.Endpoints.csproj | 1 - .../ResourceManagementController.cs | 1 - .../DataMemberAttributeValueProviderFilter.cs | 2 ++ .../Resources/ResourceEntityAccessor.cs | 25 +++++++------- .../ResourceEntityAccessorTests.cs | 33 ++++++++++++++++++- 6 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 src/Moryx.AbstractionLayer.Resources.Endpoints/DataMemberAttributeValueProviderFilter.cs diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/DataMemberAttributeValueProviderFilter.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/DataMemberAttributeValueProviderFilter.cs new file mode 100644 index 000000000..3b15660fd --- /dev/null +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/DataMemberAttributeValueProviderFilter.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System.Reflection; +using System.Runtime.Serialization; +using Moryx.Configuration; + +namespace Moryx.AbstractionLayer.Resources.Endpoints +{ + internal class DataMemberAttributeValueProviderFilter : IValueProviderFilter + { + private readonly bool _filterDataMembers; + + public DataMemberAttributeValueProviderFilter(bool filterDataMembers) + { + _filterDataMembers = filterDataMembers; + } + + public bool CheckProperty(PropertyInfo propertyInfo) + { + if (_filterDataMembers) + return propertyInfo.GetCustomAttribute() == null; + return propertyInfo.GetCustomAttribute() != null; + } + } +} diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj b/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj index c82aa2e32..8c981b0b2 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs index 32839fa53..a58a79ad8 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs @@ -17,7 +17,6 @@ using Moryx.Runtime.Modules; using Moryx.Runtime.Container; using Moryx.Configuration; -using Moryx.Resources.Management; namespace Moryx.AbstractionLayer.Resources.Endpoints { diff --git a/src/Moryx.Resources.Management/Resources/DataMemberAttributeValueProviderFilter.cs b/src/Moryx.Resources.Management/Resources/DataMemberAttributeValueProviderFilter.cs index 1027a01d3..4bfa68f6a 100644 --- a/src/Moryx.Resources.Management/Resources/DataMemberAttributeValueProviderFilter.cs +++ b/src/Moryx.Resources.Management/Resources/DataMemberAttributeValueProviderFilter.cs @@ -1,12 +1,14 @@ // Copyright (c) 2023, Phoenix Contact GmbH & Co. KG // Licensed under the Apache License, Version 2.0 +using System; using System.Reflection; using System.Runtime.Serialization; using Moryx.Configuration; namespace Moryx.Resources.Management { + [Obsolete("No longer needed within the ResourceManagement")] public class DataMemberAttributeValueProviderFilter : IValueProviderFilter { private readonly bool _filterDataMembers; diff --git a/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs b/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs index d5542932c..56519f57f 100644 --- a/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs +++ b/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs @@ -72,9 +72,8 @@ public Resource Instantiate(IResourceTypeController typeController, IResourceGra Instance.Name = Name; Instance.Description = Description; - // Copy extended data from json - if (ExtensionData != null) - JsonConvert.PopulateObject(ExtensionData, Instance, JsonSettings.Minimal); + // Copy extended data from json, or simply use JSON to provide defaults + JsonConvert.PopulateObject(ExtensionData ?? "{}", Instance, JsonSettings.Minimal); return Instance; } @@ -111,14 +110,14 @@ public static ICollection FetchResourceTemplates(IUnitOf { var resourceRepo = uow.GetRepository(); - var resources = + var resources = (from res in resourceRepo.Linq where res.Deleted == null select res) .ToList(); var resourcEntityAccessors = resources - .Select(res => + .Select(res => new ResourceEntityAccessor { Id = res.Id, @@ -127,13 +126,13 @@ public static ICollection FetchResourceTemplates(IUnitOf Description = res.Description, ExtensionData = res.ExtensionData, Relations = (from target in res.Targets - where target.Target.Deleted == null - // Attention: This is Copy&Paste because of LinQ limitations - select new ResourceRelationAccessor - { - Entity = target, - Role = ResourceReferenceRole.Target, - }).Concat( + where target.Target.Deleted == null + // Attention: This is Copy&Paste because of LinQ limitations + select new ResourceRelationAccessor + { + Entity = target, + Role = ResourceReferenceRole.Target, + }).Concat( from source in res.Sources where source.Source.Deleted == null // Attention: This is Copy&Paste because of LinQ limitations @@ -181,7 +180,7 @@ internal class ResourceRelationAccessor /// Type of the reference relation /// public ResourceRelationType RelationType => (ResourceRelationType)Entity.RelationType; - + /// /// Id of the referenced resource /// diff --git a/src/Tests/Moryx.Resources.Management.Tests/ResourceEntityAccessorTests.cs b/src/Tests/Moryx.Resources.Management.Tests/ResourceEntityAccessorTests.cs index 32b9dc8b1..f8701a9d0 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/ResourceEntityAccessorTests.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/ResourceEntityAccessorTests.cs @@ -9,6 +9,7 @@ using Moryx.Model.Repositories; using Newtonsoft.Json; using NUnit.Framework; +using System.ComponentModel; namespace Moryx.Resources.Management.Tests { @@ -22,7 +23,8 @@ public class ResourceEntityAccessorTests public void Setup() { var typeControllerMock = new Mock(); - typeControllerMock.Setup(tc => tc.Create(It.IsAny())).Returns(new TestResource()); + typeControllerMock.Setup(tc => tc.Create(It.Is(type => type == typeof(TestResource).ResourceType()))).Returns(new TestResource()); + typeControllerMock.Setup(tc => tc.Create(It.Is(type => type == typeof(DefaultTestResource).ResourceType()))).Returns(new DefaultTestResource()); _typeControllerMock = typeControllerMock.Object; @@ -30,6 +32,26 @@ public void Setup() _resourceGraph = resourceCreator.Object; } + [Test(Description = "Calling Instantiate without an entity sets default value")] + public void InstantiateWithoutEntitySetsDefaults() + { + // Arrange + var accessor = new ResourceEntityAccessor + { + Type = typeof(DefaultTestResource).ResourceType() + }; + + // Act + var resource = accessor.Instantiate(_typeControllerMock, _resourceGraph) as DefaultTestResource; + + // Assert + Assert.NotNull(resource); + Assert.AreEqual(accessor.Type, resource.GetType().ResourceType()); + + Assert.IsTrue(resource.Enabled); + Assert.AreEqual(42, resource.Number); + } + [Test(Description = "Instantiates a resource")] public void InstantiateCreatesAValidResourceObject() { @@ -118,6 +140,15 @@ private class ExtensionDataInherited : ExtensionDataTestBase public long Value3 => 42; } + private class DefaultTestResource : Resource + { + [DataMember, DefaultValue(42)] + public int Number { get; set; } + + [DataMember, DefaultValue(true)] + public bool Enabled { get; set; } + } + private class TestResource : Resource { [DataMember] From 28c2346df11098fae01f009f2a4b58abab5cc453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 06:32:29 +0000 Subject: [PATCH 62/82] Bump @mui/material from 5.16.7 to 6.1.1 in /src/Moryx.CommandCenter.Web Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 5.16.7 to 6.1.1. - [Release notes](https://github.com/mui/material-ui/releases) - [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md) - [Commits](https://github.com/mui/material-ui/commits/v6.1.1/packages/mui-material) --- updated-dependencies: - dependency-name: "@mui/material" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- src/Moryx.CommandCenter.Web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index 1d5efcddd..c3c71c3f3 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -16,7 +16,7 @@ "@emotion/styled": "^11.11.5", "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", - "@mui/material": "^5.15.15", + "@mui/material": "^6.1.1", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "@types/react-redux": "^7.1.33", From d096c9367a5d6be5be67b558453d46e2e8fa0133 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:22:24 +0000 Subject: [PATCH 63/82] Bump react-dom from 18.2.0 to 18.3.1 in /src/Moryx.CommandCenter.Web Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 18.2.0 to 18.3.1. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v18.3.1/packages/react-dom) --- updated-dependencies: - dependency-name: react-dom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/Moryx.CommandCenter.Web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moryx.CommandCenter.Web/package.json b/src/Moryx.CommandCenter.Web/package.json index c3c71c3f3..19dba0c84 100644 --- a/src/Moryx.CommandCenter.Web/package.json +++ b/src/Moryx.CommandCenter.Web/package.json @@ -25,7 +25,7 @@ "path-scurry": "^1.10.2", "query-string": "^9.0.0", "react": "18.3.1", - "react-dom": "18.2.0", + "react-dom": "18.3.1", "react-redux": "^9.1.0", "react-router": "^6.22.0", "react-router-dom": "^6.22.0", From 58661e26e760d3d49267cc58cfa4e96c38380ae2 Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Tue, 22 Oct 2024 16:17:00 +0200 Subject: [PATCH 64/82] Extend PossibleValuesAttribute with ServiceProvider --- .../Core/Serialization/PossibleValues.md | 6 ++-- ...AbstractionLayer.Products.Endpoints.csproj | 1 + .../PartialSerialization.cs | 17 +++++++++- .../ProductConverter.cs | 32 ++++++++++++------- .../ProductManagementController.cs | 24 ++++++++++---- .../ResourceManagementController.cs | 7 ++-- .../ResourceSerialization.cs | 2 +- .../Moryx.Products.Samples.csproj | 1 + .../Recipe/FacacadeRecipeValueAttribute.cs | 25 +++++++++++++++ .../Recipe/WatchProductRecipe.cs | 2 +- .../Modules/Endpoint/ModulesController.cs | 6 ++-- .../PossibleValuesSerialization.cs | 25 +++++++++++---- src/Moryx/Moryx.csproj | 1 + .../PossibleValues/PossibleValuesAttribute.cs | 27 +++++++++++++++- .../ProductConverterTests.cs | 4 +-- 15 files changed, 143 insertions(+), 37 deletions(-) create mode 100644 src/Moryx.Products.Samples/Recipe/FacacadeRecipeValueAttribute.cs diff --git a/docs/articles/Core/Serialization/PossibleValues.md b/docs/articles/Core/Serialization/PossibleValues.md index d68bb0bd8..e3d565101 100644 --- a/docs/articles/Core/Serialization/PossibleValues.md +++ b/docs/articles/Core/Serialization/PossibleValues.md @@ -26,15 +26,15 @@ public abstract class PossibleValuesAttribute : Attribute public abstract bool UpdateFromPredecessor { get; } /// - /// All possible values for this member represented as strings. The given container might be null + /// All possible values for this member represented as strings. The given containers might be null /// and can be used to resolve possible values /// - public abstract IEnumerable GetValues(IContainer container); + public virtual IEnumerable GetValues(IContainer container, IServiceProvider serviceProvider); /// /// String to value conversion. Must be override if is set to true"/> /// - public virtual object Parse(IContainer container, string value) + public virtual object Parse(IContainer container, IServiceProvider serviceProvider), string value) { return value; } diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/Moryx.AbstractionLayer.Products.Endpoints.csproj b/src/Moryx.AbstractionLayer.Products.Endpoints/Moryx.AbstractionLayer.Products.Endpoints.csproj index 4e5cd128d..82b4823b5 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/Moryx.AbstractionLayer.Products.Endpoints.csproj +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/Moryx.AbstractionLayer.Products.Endpoints.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/PartialSerialization.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/PartialSerialization.cs index d8d510dfc..42adb63f5 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/PartialSerialization.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/PartialSerialization.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Moryx.Configuration; +using Moryx.Container; using Moryx.Serialization; namespace Moryx.AbstractionLayer.Products.Endpoints @@ -28,7 +29,21 @@ static PartialSerialization() _serialization = new EntrySerializeSerialization(); } - public PartialSerialization() : base(null, new EmptyValueProvider()) + /// + /// Creates a new instance + /// + [Obsolete("Use serialization with containers instead")] + public PartialSerialization() : this(null, null) + { + } + + /// + /// Create serialization with access to global and local container + /// + /// + /// + public PartialSerialization(IContainer localContainer, IServiceProvider serviceProvider) + : base(localContainer, serviceProvider, new EmptyValueProvider()) { } diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs index 545315451..516b89022 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0 using Moryx.AbstractionLayer.Recipes; +using Moryx.Container; using Moryx.Serialization; using Moryx.Tools; using Moryx.Workplans; @@ -20,12 +21,19 @@ public class ProductConverter // Null object pattern for identity private static readonly ProductIdentity EmptyIdentity = new ProductIdentity(string.Empty, 0); - private static readonly ICustomSerialization ProductSerialization = new PartialSerialization(); - private static readonly ICustomSerialization RecipeSerialization = new PartialSerialization(); + private readonly ICustomSerialization _productSerialization; + private readonly ICustomSerialization _recipeSerialization; - public ProductConverter(IProductManagement productManagement) + public IContainer ProductManagerContainer { get; } + public IServiceProvider GlobalContainer { get; } + + public ProductConverter(IProductManagement productManagement, IContainer localContainer, IServiceProvider globalContainer) { _productManagement = productManagement; + ProductManagerContainer = localContainer; + GlobalContainer = globalContainer; + _productSerialization = new PartialSerialization(localContainer, globalContainer); + _recipeSerialization = new PartialSerialization(localContainer, globalContainer); } public ProductDefinitionModel ConvertProductType(Type productType) @@ -39,7 +47,7 @@ public ProductDefinitionModel ConvertProductType(Type productType) Name = productType.FullName, DisplayName = productType.GetDisplayName() ?? productType.Name, BaseDefinition = baseTypeName, - Properties = EntryConvert.EncodeClass(productType, ProductSerialization) + Properties = EntryConvert.EncodeClass(productType, _productSerialization) }; } @@ -73,7 +81,7 @@ public ProductModel ConvertProduct(IProductType productType, bool flat) // Properties var typeWrapper = _productManagement.GetTypeWrapper(productType.GetType().FullName); var properties = typeWrapper != null ? typeWrapper.Properties.ToArray() : productType.GetType().GetProperties(); - converted.Properties = EntryConvert.EncodeObject(productType, ProductSerialization); + converted.Properties = EntryConvert.EncodeObject(productType, _productSerialization); // Files converted.Files = ConvertFiles(productType, properties); @@ -124,7 +132,7 @@ private void ConvertParts(IProductType productType, IEnumerable pr DisplayName = displayName, Type = FetchProductType(property.PropertyType), Parts = partModel is null ? new PartModel[0] : new[] { partModel }, - PropertyTemplates = EntryConvert.EncodeClass(property.PropertyType, ProductSerialization) + PropertyTemplates = EntryConvert.EncodeClass(property.PropertyType, _productSerialization) }; connectors.Add(connector); } @@ -139,7 +147,7 @@ private void ConvertParts(IProductType productType, IEnumerable pr DisplayName = displayName, Type = FetchProductType(linkType), Parts = links?.Select(ConvertPart).ToArray(), - PropertyTemplates = EntryConvert.EncodeClass(linkType, ProductSerialization) + PropertyTemplates = EntryConvert.EncodeClass(linkType, _productSerialization) }; connectors.Add(connector); } @@ -165,7 +173,7 @@ private PartModel ConvertPart(IProductPartLink link) { Id = link.Id, Product = ConvertProduct(link.Product, true), - Properties = EntryConvert.EncodeObject(link, ProductSerialization) + Properties = EntryConvert.EncodeObject(link, _productSerialization) }; return part; } @@ -214,7 +222,7 @@ public IProductType ConvertProductBack(ProductModel source, ProductType converte // Copy extended properties var typeWrapper = _productManagement.GetTypeWrapper(converted.GetType().FullName); var properties = typeWrapper != null ? typeWrapper.Properties.ToArray() : converted.GetType().GetProperties(); - EntryConvert.UpdateInstance(converted, source.Properties, ProductSerialization); + EntryConvert.UpdateInstance(converted, source.Properties, _productSerialization); // Copy Files ConvertFilesBack(converted, source, properties); @@ -318,7 +326,7 @@ private static void ConvertFilesBack(object converted, ProductModel product, Pro } } - public static RecipeModel ConvertRecipe(IRecipe recipe) + public RecipeModel ConvertRecipe(IRecipe recipe) { // Transform to DTO and transmit var converted = new RecipeModel @@ -328,7 +336,7 @@ public static RecipeModel ConvertRecipe(IRecipe recipe) Type = recipe.GetType().Name, State = recipe.State, Revision = recipe.Revision, - Properties = EntryConvert.EncodeObject(recipe, RecipeSerialization), + Properties = EntryConvert.EncodeObject(recipe, _recipeSerialization), IsClone = recipe.Classification.HasFlag(RecipeClassification.Clone) }; @@ -377,7 +385,7 @@ public IProductRecipe ConvertRecipeBack(RecipeModel recipe, IProductRecipe produ productRecipe.Product = productType; } - EntryConvert.UpdateInstance(productRecipe, recipe.Properties, RecipeSerialization); + EntryConvert.UpdateInstance(productRecipe, recipe.Properties, _recipeSerialization); // Do not update a clones classification if (productRecipe.Classification.HasFlag(RecipeClassification.Clone)) diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs index af0aaea26..3689aed48 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs @@ -14,6 +14,11 @@ using Moryx.Configuration; using Moryx.Asp.Extensions; using Moryx.AbstractionLayer.Properties; +using Microsoft.Extensions.DependencyInjection; +using Moryx.AbstractionLayer.Resources; +using Moryx.Runtime.Modules; +using System.ComponentModel; +using Moryx.Runtime.Container; namespace Moryx.AbstractionLayer.Products.Endpoints { @@ -27,10 +32,15 @@ public class ProductManagementController : ControllerBase { private readonly IProductManagement _productManagement; private readonly ProductConverter _productConverter; - public ProductManagementController(IProductManagement productManagement) + public ProductManagementController(IProductManagement productManagement, + IModuleManager moduleManager, + IServiceProvider serviceProvider) { _productManagement = productManagement; - _productConverter = new ProductConverter(_productManagement); + + var module = moduleManager.AllModules.FirstOrDefault(module => module is IFacadeContainer); + var host = (IContainerHost)module; + _productConverter = new ProductConverter(_productManagement, host.Container, serviceProvider); } #region importers @@ -39,6 +49,8 @@ public ProductManagementController(IProductManagement productManagement) [Authorize(Policy = ProductPermissions.CanViewTypes)] public ActionResult GetProductCustomization() { + var parameterSerialization = new PossibleValuesSerialization(_productConverter.ProductManagerContainer, _productConverter.GlobalContainer, + new ValueProviderExecutor(new ValueProviderExecutorSettings().AddDefaultValueProvider())); return new ProductCustomization { ProductTypes = GetProductTypes(), @@ -46,7 +58,7 @@ public ActionResult GetProductCustomization() Importers = _productManagement.Importers.Select(i => new ProductImporter { Name = i.Key, - Parameters = EntryConvert.EncodeObject(i.Value, new PossibleValuesSerialization(null, new ValueProviderExecutor(new ValueProviderExecutorSettings().AddDefaultValueProvider()))) + Parameters = EntryConvert.EncodeObject(i.Value, parameterSerialization) }).ToArray() }; } @@ -247,7 +259,7 @@ public ActionResult GetRecipes(long id, int classification) var recipes = _productManagement.GetRecipes(productType, (RecipeClassification)classification); var recipeModels = new List(); foreach (var recipe in recipes) - recipeModels.Add(ProductConverter.ConvertRecipe(recipe)); + recipeModels.Add(_productConverter.ConvertRecipe(recipe)); return recipeModels.ToArray(); } #endregion @@ -330,7 +342,7 @@ public ActionResult GetRecipe(long id) var recipe = _productManagement.LoadRecipe(id); if (recipe == null) return NotFound(new MoryxExceptionResponse {Title= string.Format(Strings.RecipeNotFoundException_Message, id) }); - return ProductConverter.ConvertRecipe(recipe); + return _productConverter.ConvertRecipe(recipe); } [HttpPost] @@ -380,7 +392,7 @@ public ActionResult CreateRecipe(string recipeType) var recipe = _productManagement.CreateRecipe(recipeType); if (recipe == null) recipe = (IProductRecipe)TypeTool.CreateInstance(recipeType); - return ProductConverter.ConvertRecipe(recipe); + return _productConverter.ConvertRecipe(recipe); } #endregion } diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs index a58a79ad8..29b106227 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs @@ -32,13 +32,16 @@ public class ResourceModificationController : ControllerBase private readonly IResourceTypeTree _resourceTypeTree; private readonly ResourceSerialization _serialization; - public ResourceModificationController(IResourceManagement resourceManagement, IResourceTypeTree resourceTypeTree, IModuleManager moduleManager) + public ResourceModificationController(IResourceManagement resourceManagement, + IResourceTypeTree resourceTypeTree, + IModuleManager moduleManager, + IServiceProvider serviceProvider) { _resourceManagement = resourceManagement ?? throw new ArgumentNullException(nameof(resourceManagement)); _resourceTypeTree = resourceTypeTree ?? throw new ArgumentNullException(nameof(resourceTypeTree)); var module = moduleManager.AllModules.FirstOrDefault(module => module is IFacadeContainer); var containerHost = (IContainerHost)module; - _serialization = new ResourceSerialization(containerHost.Container); + _serialization = new ResourceSerialization(containerHost.Container, serviceProvider); } [HttpGet] diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceSerialization.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceSerialization.cs index 2af89c41a..cf4eb70d5 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceSerialization.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceSerialization.cs @@ -18,7 +18,7 @@ internal class ResourceSerialization : PossibleValuesSerialization /// private EntrySerializeSerialization _memberFilter = new(); - public ResourceSerialization(IContainer container) : base(container, new ValueProviderExecutor(new ValueProviderExecutorSettings())) + public ResourceSerialization(IContainer container, IServiceProvider serviceProvider) : base(container, serviceProvider, new ValueProviderExecutor(new ValueProviderExecutorSettings())) { } diff --git a/src/Moryx.Products.Samples/Moryx.Products.Samples.csproj b/src/Moryx.Products.Samples/Moryx.Products.Samples.csproj index 5208563fb..3a434c7ef 100644 --- a/src/Moryx.Products.Samples/Moryx.Products.Samples.csproj +++ b/src/Moryx.Products.Samples/Moryx.Products.Samples.csproj @@ -8,6 +8,7 @@ + \ No newline at end of file diff --git a/src/Moryx.Products.Samples/Recipe/FacacadeRecipeValueAttribute.cs b/src/Moryx.Products.Samples/Recipe/FacacadeRecipeValueAttribute.cs new file mode 100644 index 000000000..6e3412027 --- /dev/null +++ b/src/Moryx.Products.Samples/Recipe/FacacadeRecipeValueAttribute.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Moryx.Container; +using Moryx.Serialization; +using Moryx.TestModule; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Moryx.Products.Samples.Recipe +{ + public class FacacadeRecipeValueAttribute : PossibleValuesAttribute + { + public override bool OverridesConversion => false; + + public override bool UpdateFromPredecessor => false; + + public override IEnumerable GetValues(IContainer localContainer, IServiceProvider serviceProvider) + { + var module = serviceProvider.GetRequiredService(); + return new[] { module.Bla.ToString("D") }; + } + } +} diff --git a/src/Moryx.Products.Samples/Recipe/WatchProductRecipe.cs b/src/Moryx.Products.Samples/Recipe/WatchProductRecipe.cs index d1f33565c..4fbcd4e52 100644 --- a/src/Moryx.Products.Samples/Recipe/WatchProductRecipe.cs +++ b/src/Moryx.Products.Samples/Recipe/WatchProductRecipe.cs @@ -21,7 +21,7 @@ public WatchProductRecipe(WatchProductRecipe source) : base(source) Case = source.Case; } - [EntrySerialize] + [EntrySerialize, FacacadeRecipeValue] [DisplayName("Cores Installed")] public int CoresInstalled { get; set; } diff --git a/src/Moryx.Runtime.Endpoints/Modules/Endpoint/ModulesController.cs b/src/Moryx.Runtime.Endpoints/Modules/Endpoint/ModulesController.cs index b8e2e3990..dbf2455f4 100644 --- a/src/Moryx.Runtime.Endpoints/Modules/Endpoint/ModulesController.cs +++ b/src/Moryx.Runtime.Endpoints/Modules/Endpoint/ModulesController.cs @@ -26,12 +26,14 @@ public class ModulesController : ControllerBase private readonly IModuleManager _moduleManager; private readonly IConfigManager _configManager; private readonly IParallelOperations _parallelOperations; + private readonly IServiceProvider _serviceProvider; - public ModulesController(IModuleManager moduleManager, IConfigManager configManager, IParallelOperations parallelOperations) + public ModulesController(IModuleManager moduleManager, IConfigManager configManager, IParallelOperations parallelOperations, IServiceProvider serviceProvider) { _moduleManager = moduleManager; _configManager = configManager; _parallelOperations = parallelOperations; + _serviceProvider = serviceProvider; } [HttpGet("dependencies")] @@ -254,7 +256,7 @@ private ICustomSerialization CreateSerialization(IModule module) { var host = (IContainerHost)module; // TODO: This is dangerous - return new PossibleValuesSerialization(host.Container, (IEmptyPropertyProvider)_configManager) + return new PossibleValuesSerialization(host.Container, _serviceProvider, (IEmptyPropertyProvider)_configManager) { FormatProvider = Thread.CurrentThread.CurrentUICulture }; diff --git a/src/Moryx/Configuration/PossibleValuesSerialization.cs b/src/Moryx/Configuration/PossibleValuesSerialization.cs index 0c1b07112..fc9987f3a 100644 --- a/src/Moryx/Configuration/PossibleValuesSerialization.cs +++ b/src/Moryx/Configuration/PossibleValuesSerialization.cs @@ -21,6 +21,11 @@ public class PossibleValuesSerialization : DefaultSerialization /// protected IContainer Container { get; } + /// + /// Access to level 1 service registration + /// + public IServiceProvider ServiceProvider { get; } + /// /// Empty property provider to pre-fill newley created objects /// @@ -29,9 +34,17 @@ public class PossibleValuesSerialization : DefaultSerialization /// /// Initialize base class /// + [Obsolete("Construct possible values with ServiceProvider for attributes that rely on it")] public PossibleValuesSerialization(IContainer container, IEmptyPropertyProvider emptyPropertyProvider) + :this(container, null, emptyPropertyProvider) { } + + /// + /// Initialize base class + /// + public PossibleValuesSerialization(IContainer container, IServiceProvider serviceProvider, IEmptyPropertyProvider emptyPropertyProvider) { Container = container; + ServiceProvider = serviceProvider; EmptyPropertyProvider = emptyPropertyProvider; } @@ -45,9 +58,9 @@ public override EntryPrototype[] Prototypes(Type memberType, ICustomAttributePro // Create prototypes from possible values var list = new List(); - foreach (var value in possibleValuesAtt.GetValues(Container)) + foreach (var value in possibleValuesAtt.GetValues(Container, ServiceProvider)) { - var prototype = possibleValuesAtt.Parse(Container, value); + var prototype = possibleValuesAtt.Parse(Container, ServiceProvider, value); EmptyPropertyProvider.FillEmpty(prototype); list.Add(new EntryPrototype(value, prototype)); } @@ -63,7 +76,7 @@ public override string[] PossibleValues(Type memberType, ICustomAttributeProvide return base.PossibleValues(memberType, attributeProvider); // Use attribute - var values = valuesAttribute.GetValues(Container); + var values = valuesAttribute.GetValues(Container, ServiceProvider); return values?.Distinct().ToArray(); } @@ -90,7 +103,7 @@ public override string[] PossibleElementValues(Type memberType, ICustomAttribute } // Use attribute - var values = valuesAttribute.GetValues(Container); + var values = valuesAttribute.GetValues(Container, ServiceProvider); return values?.Distinct().ToArray(); } @@ -99,7 +112,7 @@ public override object CreateInstance(Type memberType, ICustomAttributeProvider { var possibleValuesAtt = attributeProvider.GetCustomAttribute(); var instance = possibleValuesAtt != null - ? possibleValuesAtt.Parse(Container, encoded.Value.Current) + ? possibleValuesAtt.Parse(Container, ServiceProvider, encoded.Value.Current) : base.CreateInstance(memberType, attributeProvider, encoded); EmptyPropertyProvider.FillEmpty(instance); @@ -120,7 +133,7 @@ public override object ConvertValue(Type memberType, ICustomAttributeProvider at if (value.Type == EntryValueType.Class && currentValue != null && currentValue.GetType().Name == value.Current) return currentValue; - var instance = att.Parse(Container, mappedEntry.Value.Current); + var instance = att.Parse(Container, ServiceProvider, mappedEntry.Value.Current); if (mappedEntry.Value.Type == EntryValueType.Class) { EmptyPropertyProvider.FillEmpty(instance); diff --git a/src/Moryx/Moryx.csproj b/src/Moryx/Moryx.csproj index 76d3188cd..1d16f9294 100644 --- a/src/Moryx/Moryx.csproj +++ b/src/Moryx/Moryx.csproj @@ -7,6 +7,7 @@ Core package of the MORYX ecosystem. It defines the necessary types for MORYX compatibility as well as commonly required tools. true MORYX;Serialization;Configuration;Logging;Core;Modules;Workplans + true diff --git a/src/Moryx/Serialization/PossibleValues/PossibleValuesAttribute.cs b/src/Moryx/Serialization/PossibleValues/PossibleValuesAttribute.cs index 328682bbe..144c28c8a 100644 --- a/src/Moryx/Serialization/PossibleValues/PossibleValuesAttribute.cs +++ b/src/Moryx/Serialization/PossibleValues/PossibleValuesAttribute.cs @@ -27,14 +27,39 @@ public abstract class PossibleValuesAttribute : Attribute /// All possible values for this member represented as strings. The given container might be null /// and can be used to resolve possible values /// - public abstract IEnumerable GetValues(IContainer container); + [Obsolete("Replaced by PossibleValues with access to global service registration")] + public virtual IEnumerable GetValues(IContainer container) + { + return Array.Empty(); + } + + /// + /// Extract possible values from local or global DI registration + /// + // TODO: Make abstract in MORYX 10 + public virtual IEnumerable GetValues(IContainer localContainer, IServiceProvider serviceProvider) + { + return GetValues(localContainer); + } /// /// String to value conversion /// + [Obsolete("Replaced by Parse with ServiceProvider reference")] public virtual object Parse(IContainer container, string value) { return value; } + + /// + /// Parse value from string using local or global DI container + /// + /// Module local DI container + /// Global service registration + /// Value to parse + public virtual object Parse(IContainer container, IServiceProvider serviceProvider, string value) + { + return Parse(container, value); + } } } diff --git a/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs b/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs index 403e99218..a5f14bb6d 100644 --- a/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs +++ b/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs @@ -27,7 +27,7 @@ public void Setup() { _productManagerMock = new Mock(); - _productConverter = new ProductConverter(_productManagerMock.Object); + _productConverter = new ProductConverter(_productManagerMock.Object, null, null); } #region Products @@ -227,7 +227,7 @@ public void ForwardBackwardRecipeConversionWithoutInformationLoss(DummyProductRe // Act - var convertedModel = ProductConverter.ConvertRecipe(originalRecipe); + var convertedModel = _productConverter.ConvertRecipe(originalRecipe); var recoveredOriginal = _productConverter.ConvertRecipeBack(convertedModel, targetDummyRecipe, backupProductType); From e767bdf4baf55ae7a81f69356984778211ce6416 Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Wed, 23 Oct 2024 08:38:06 +0200 Subject: [PATCH 65/82] Fix build with adjusted namespace and increment version --- VERSION | 2 +- .../ProductManagementController.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 8104cabd3..fbb9ea12d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.1.0 +8.2.0 diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs index 963731e65..5f41351e3 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs @@ -18,7 +18,6 @@ using Moryx.AbstractionLayer.Resources; using Moryx.Runtime.Modules; using System.ComponentModel; -using Moryx.Runtime.Container; namespace Moryx.AbstractionLayer.Products.Endpoints { @@ -39,8 +38,7 @@ public ProductManagementController(IProductManagement productManagement, _productManagement = productManagement; var module = moduleManager.AllModules.FirstOrDefault(module => module is IFacadeContainer); - var host = (IContainerHost)module; - _productConverter = new ProductConverter(_productManagement, host.Container, serviceProvider); + _productConverter = new ProductConverter(_productManagement, module.Container, serviceProvider); } #region importers From 7a8e6f637957cdbc1c2ea2a9e19bd855fa210eef Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Wed, 23 Oct 2024 09:40:19 +0200 Subject: [PATCH 66/82] Fix artifact action --- .github/workflows/build-and-test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 39cb1803b..3b890f554 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -43,7 +43,7 @@ jobs: run: ./Build.ps1 -Build -Pack - name: Upload package artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: packages path: artifacts/Packages/ @@ -72,7 +72,7 @@ jobs: run: ./Build.ps1 -UnitTests - name: Upload test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: test-results path: artifacts/Tests/ @@ -101,7 +101,7 @@ jobs: run: ./Build.ps1 -IntegrationTests - name: Upload test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: test-results path: artifacts/Tests/ @@ -112,7 +112,7 @@ jobs: runs-on: windows-2019 steps: - name: Download test results - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: test-results path: artifacts/Tests/ @@ -133,7 +133,7 @@ jobs: run: ./Build.ps1 -GenerateDocs - name: Upload documentation results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: documentation path: artifacts/Documentation/ @@ -159,7 +159,7 @@ jobs: dotnet-version: '6' - name: Download package artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: packages path: artifacts/Packages/ From b9bb4f58add6c103c9949e23eb3086dc50c6b299 Mon Sep 17 00:00:00 2001 From: Thomas Fuchs Date: Wed, 23 Oct 2024 11:20:39 +0200 Subject: [PATCH 67/82] Applay review comments --- .../ProductConverter.cs | 9 +++++++-- .../ProductManagementController.cs | 10 +++++----- src/Moryx/Moryx.csproj | 1 - .../ProductConverterTests.cs | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs index 516b89022..d1b5c14d5 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductConverter.cs @@ -326,7 +326,12 @@ private static void ConvertFilesBack(object converted, ProductModel product, Pro } } - public RecipeModel ConvertRecipe(IRecipe recipe) + [Obsolete("Use ConvertRecipe on instance")] + public static RecipeModel ConvertRecipe(IRecipe recipe) => ConvertRecipe(recipe, new PartialSerialization(null, null)); + + public RecipeModel ConvertRecipeV2(IRecipe recipe) => ConvertRecipe(recipe, _recipeSerialization); + + private static RecipeModel ConvertRecipe(IRecipe recipe, ICustomSerialization serialization) { // Transform to DTO and transmit var converted = new RecipeModel @@ -336,7 +341,7 @@ public RecipeModel ConvertRecipe(IRecipe recipe) Type = recipe.GetType().Name, State = recipe.State, Revision = recipe.Revision, - Properties = EntryConvert.EncodeObject(recipe, _recipeSerialization), + Properties = EntryConvert.EncodeObject(recipe, serialization), IsClone = recipe.Classification.HasFlag(RecipeClassification.Clone) }; diff --git a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs index 3689aed48..71654e272 100644 --- a/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs +++ b/src/Moryx.AbstractionLayer.Products.Endpoints/ProductManagementController.cs @@ -38,8 +38,8 @@ public ProductManagementController(IProductManagement productManagement, { _productManagement = productManagement; - var module = moduleManager.AllModules.FirstOrDefault(module => module is IFacadeContainer); - var host = (IContainerHost)module; + var module = moduleManager.AllModules.FirstOrDefault(module => module is IFacadeContainer); + var host = (IContainerHost)module; _productConverter = new ProductConverter(_productManagement, host.Container, serviceProvider); } @@ -259,7 +259,7 @@ public ActionResult GetRecipes(long id, int classification) var recipes = _productManagement.GetRecipes(productType, (RecipeClassification)classification); var recipeModels = new List(); foreach (var recipe in recipes) - recipeModels.Add(_productConverter.ConvertRecipe(recipe)); + recipeModels.Add(_productConverter.ConvertRecipeV2(recipe)); return recipeModels.ToArray(); } #endregion @@ -342,7 +342,7 @@ public ActionResult GetRecipe(long id) var recipe = _productManagement.LoadRecipe(id); if (recipe == null) return NotFound(new MoryxExceptionResponse {Title= string.Format(Strings.RecipeNotFoundException_Message, id) }); - return _productConverter.ConvertRecipe(recipe); + return _productConverter.ConvertRecipeV2(recipe); } [HttpPost] @@ -392,7 +392,7 @@ public ActionResult CreateRecipe(string recipeType) var recipe = _productManagement.CreateRecipe(recipeType); if (recipe == null) recipe = (IProductRecipe)TypeTool.CreateInstance(recipeType); - return _productConverter.ConvertRecipe(recipe); + return _productConverter.ConvertRecipeV2(recipe); } #endregion } diff --git a/src/Moryx/Moryx.csproj b/src/Moryx/Moryx.csproj index 1d16f9294..76d3188cd 100644 --- a/src/Moryx/Moryx.csproj +++ b/src/Moryx/Moryx.csproj @@ -7,7 +7,6 @@ Core package of the MORYX ecosystem. It defines the necessary types for MORYX compatibility as well as commonly required tools. true MORYX;Serialization;Configuration;Logging;Core;Modules;Workplans - true diff --git a/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs b/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs index a5f14bb6d..2d6338722 100644 --- a/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs +++ b/src/Tests/Moryx.Products.Management.Tests/ProductConverterTests.cs @@ -227,7 +227,7 @@ public void ForwardBackwardRecipeConversionWithoutInformationLoss(DummyProductRe // Act - var convertedModel = _productConverter.ConvertRecipe(originalRecipe); + var convertedModel = _productConverter.ConvertRecipeV2(originalRecipe); var recoveredOriginal = _productConverter.ConvertRecipeBack(convertedModel, targetDummyRecipe, backupProductType); From bbc8b5fa7f4a3d6d0a3f6cb39e57703649140a84 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Thu, 7 Nov 2024 18:24:36 +0100 Subject: [PATCH 68/82] Fix name and description parsing in the resource type serialization --- .../Converter/ResourceToModelConverter.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/Converter/ResourceToModelConverter.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/Converter/ResourceToModelConverter.cs index 31a365202..daa3c0276 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/Converter/ResourceToModelConverter.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/Converter/ResourceToModelConverter.cs @@ -76,7 +76,7 @@ protected ResourceModel ToModel(Resource current, bool partially) Description = current.Description, // Use simplified type reference - Type = current.ResourceType() + Type = current.ResourceType(), }; // Set partial flag or load complex properties depending on details depth @@ -197,14 +197,8 @@ internal ResourceTypeModel ConvertType(IResourceTypeNode node, ResourceTypeModel Creatable = node.Creatable, Name = node.Name, BaseType = baseType?.Name, - - // Read display name of the type otherwise use type short name - DisplayName = resType.GetCustomAttribute(false)?.DisplayName ?? - Regex.Replace(resType.Name, @"`\d", string.Empty), - - // Read description of the type - Description = resType.GetCustomAttribute(false)?.Description, - + DisplayName = resType.GetDisplayName(), + Description = resType.GetDescription(), // Convert resource constructors Constructors = node.Constructors.Select(ctr => EntryConvert.EncodeMethod(ctr, Serialization)).ToArray() }; From 8a95b7d807ee8487a35fe981f2962dd16ad29ce4 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Tue, 19 Nov 2024 08:04:51 +0100 Subject: [PATCH 69/82] Pass DeadEndValidation for unconnected connectors --- src/Moryx/Workflows/Validation/DeadEndValidator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Moryx/Workflows/Validation/DeadEndValidator.cs b/src/Moryx/Workflows/Validation/DeadEndValidator.cs index f8d6ef5f5..1d12e01a8 100644 --- a/src/Moryx/Workflows/Validation/DeadEndValidator.cs +++ b/src/Moryx/Workflows/Validation/DeadEndValidator.cs @@ -28,6 +28,10 @@ public bool Validate(IWorkplan workplan, ICollection errors) if(connector.Classification.HasFlag(NodeClassification.Exit)) continue; + // or if it is not used as an output anywhere and is not a start connector + if (!workplan.Steps.Any(step => step.Outputs.Contains(connector)) && !connector.Classification.HasFlag(NodeClassification.Entry)) + continue; + errors.Add(new DeadEndValidationError(connector.Id)); } return errors.Count == 0; From 9de6a3eb678cfae6358b5160b4b48fcf05c4a26e Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Tue, 19 Nov 2024 07:29:56 +0100 Subject: [PATCH 70/82] Add propper 400 response to update and save actions in resource controller --- .../ResourceManagementController.cs | 60 ++++++++++++------- .../Serialization/DefaultSerialization.cs | 14 ++++- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs index 29b106227..db75310fd 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs @@ -17,6 +17,7 @@ using Moryx.Runtime.Modules; using Moryx.Runtime.Container; using Moryx.Configuration; +using System.Runtime.Serialization; namespace Moryx.AbstractionLayer.Resources.Endpoints { @@ -170,24 +171,32 @@ private ActionResult Construct(string type, MethodEntry method) [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status417ExpectationFailed)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] [Authorize(Policy = ResourcePermissions.CanAdd)] public ActionResult Save(ResourceModel model) { - if (_resourceManagement.GetAllResources(r => r.Id == model.Id).Count() > 0) + if (_resourceManagement.GetAllResources(r => r.Id == model.Id).Any()) return Conflict($"The resource '{model.Id}' already exists."); + try + { + var id = _resourceManagement.Create(_resourceTypeTree[model.Type].ResourceType, r => { + var resourcesToSave = new HashSet(); + var resourceCache = new Dictionary(); + FromModel(model, resourcesToSave, resourceCache, r); + resourcesToSave.Skip(1).ForEach(id => _resourceManagement.Modify(id, r => true)); + }); - var id = _resourceManagement.Create(_resourceTypeTree[model.Type].ResourceType, r => { - var resourcesToSave = new HashSet(); - var resourceCache = new Dictionary(); - FromModel(model, resourcesToSave, resourceCache, r); - resourcesToSave.Skip(1).ForEach(id => _resourceManagement.Modify(id, r => true )); - }); - - return GetDetails(id); + return Ok(GetDetails(id)); + } + catch (Exception e) + { + if (e is ArgumentException or SerializationException) + return BadRequest(e.Message); + throw; + } } - /// /// Convert ResourceModel back to resource and/or update its properties /// @@ -309,8 +318,9 @@ private void UpdateReferences(Resource instance, HashSet resourcesToSave, [HttpPut] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status417ExpectationFailed)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] [Route("{id}")] [Authorize(Policy = ResourcePermissions.CanEdit)] public ActionResult Update(long id, ResourceModel model) @@ -318,15 +328,25 @@ public ActionResult Update(long id, ResourceModel model) if (_resourceManagement.GetAllResources(r=>r.Id == id) is null) return NotFound(new MoryxExceptionResponse { Title = string.Format(Strings.ResourceNotFoundException_ById_Message, id) }); - _resourceManagement.Modify(id, r => { - var resourcesToSave = new HashSet(); - var resourceCache = new Dictionary(); - FromModel(model, resourcesToSave, resourceCache, r); - resourcesToSave.ForEach(id => _resourceManagement.Modify(id, r => true)); - return true; - }); + try + { + _resourceManagement.Modify(id, r => + { + var resourcesToSave = new HashSet(); + var resourceCache = new Dictionary(); + FromModel(model, resourcesToSave, resourceCache, r); + resourcesToSave.ForEach(id => _resourceManagement.Modify(id, r => true)); + return true; + }); + } + catch (Exception e) + { + if (e is ArgumentException or SerializationException) + return BadRequest(e.Message); + throw; + } - return GetDetails(id); + return Ok(GetDetails(id)); } [HttpDelete] diff --git a/src/Moryx/Serialization/DefaultSerialization.cs b/src/Moryx/Serialization/DefaultSerialization.cs index c5508166f..490f728eb 100644 --- a/src/Moryx/Serialization/DefaultSerialization.cs +++ b/src/Moryx/Serialization/DefaultSerialization.cs @@ -221,7 +221,19 @@ public virtual object ConvertValue(Type memberType, ICustomAttributeProvider att return CollectionBuilder(memberType, currentValue, mappedEntry); default: var value = mappedEntry.Value.Current; - return value == null ? null : EntryConvert.ToObject(memberType, value, FormatProvider); + if (value is null) + return null; + + try + { + return EntryConvert.ToObject(memberType, value, FormatProvider); + } + catch (Exception e) + { + if (e is FormatException or OverflowException) + throw new ArgumentException($"Invalid value {mappedEntry.Value.Current} for entry {mappedEntry.DisplayName ?? mappedEntry.Identifier}", e); + throw; + } } } /// From e601b4b5eb8e568a3a7ccf734a9bf6573251a309 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Thu, 7 Nov 2024 18:26:29 +0100 Subject: [PATCH 71/82] Fix missing Id and Name assignements when creating a new relation. When creating a bidirectional relation the missing properties on the relation to be created caused an exception when creating the backlink. --- .../Converter/ResourceToModelConverter.cs | 2 +- .../Moryx.Resources.Management.csproj | 1 + .../Resources/ResourceEntityAccessor.cs | 4 +- .../Resources/ResourceLinker.cs | 72 +++++++++++-------- .../Mocks/ReferenceResource.cs | 13 +++- .../ResourceLinkerTests.cs | 61 +++++++++++++++- 6 files changed, 118 insertions(+), 35 deletions(-) diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/Converter/ResourceToModelConverter.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/Converter/ResourceToModelConverter.cs index daa3c0276..139d3e834 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/Converter/ResourceToModelConverter.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/Converter/ResourceToModelConverter.cs @@ -197,7 +197,7 @@ internal ResourceTypeModel ConvertType(IResourceTypeNode node, ResourceTypeModel Creatable = node.Creatable, Name = node.Name, BaseType = baseType?.Name, - DisplayName = resType.GetDisplayName(), + DisplayName = resType.GetDisplayName() ?? Regex.Replace(resType.Name, @"`\d", string.Empty), Description = resType.GetDescription(), // Convert resource constructors Constructors = node.Constructors.Select(ctr => EntryConvert.EncodeMethod(ctr, Serialization)).ToArray() diff --git a/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj b/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj index 5e84cc851..fb46a416c 100644 --- a/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj +++ b/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj @@ -3,6 +3,7 @@ net6.0 true + true ResourceManagement module composing and maintaining the resource graph as the habitat for digital twins of manufacturing assets. true MORYX;IIoT;IoT;Manufacturing;API;Resource diff --git a/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs b/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs index 56519f57f..3bd569b90 100644 --- a/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs +++ b/src/Moryx.Resources.Management/Resources/ResourceEntityAccessor.cs @@ -182,12 +182,12 @@ internal class ResourceRelationAccessor public ResourceRelationType RelationType => (ResourceRelationType)Entity.RelationType; /// - /// Id of the referenced resource + /// Id of the resource with the specified in the relation /// public long ReferenceId => Role == ResourceReferenceRole.Target ? Entity.TargetId : Entity.SourceId; /// - /// References entity + /// Entity of the resource with the specified in the relation /// public ResourceEntity ReferenceEntity => Role == ResourceReferenceRole.Target ? Entity.Target : Entity.Source; diff --git a/src/Moryx.Resources.Management/Resources/ResourceLinker.cs b/src/Moryx.Resources.Management/Resources/ResourceLinker.cs index f9fb691aa..e0aae131f 100644 --- a/src/Moryx.Resources.Management/Resources/ResourceLinker.cs +++ b/src/Moryx.Resources.Management/Resources/ResourceLinker.cs @@ -98,15 +98,15 @@ private void SaveReferences(ReferenceSaverContext context, Resource instance, Di if(dict != null) dict.Add(instance, entity); - var relations = ResourceRelationAccessor.FromEntity(context.UnitOfWork, entity) + var referenceAccessors = ResourceRelationAccessor.FromEntity(context.UnitOfWork, entity) .Union(ResourceRelationAccessor.FromQueryable(context.CreatedRelations.AsQueryable(), entity)) .ToList(); var createdResources = new List(); foreach (var referenceProperty in ReferenceProperties(instance.GetType(), false)) { - var matches = MatchingRelations(relations, referenceProperty); - var typeMatches = TypeFilter(matches, referenceProperty, context.ResolveReference).ToList(); + var matches = MatchingRelations(referenceAccessors, referenceProperty); + var typeMatches = TypeFilter(matches, referenceProperty, context.ResolveReferencedResource).ToList(); if (typeof(IEnumerable).IsAssignableFrom(referenceProperty.PropertyType)) { // Save a collection reference @@ -135,7 +135,7 @@ public IReadOnlyList SaveSingleCollection(IUnitOfWork uow, Resource in var context = new ReferenceSaverContext(uow, Graph, instance, entity); var matches = MatchingRelations(relations, property); - var typeMatches = TypeFilter(matches, property, context.ResolveReference).ToList(); + var typeMatches = TypeFilter(matches, property, context.ResolveReferencedResource).ToList(); var created = UpdateCollectionReference(context, entity, instance, property, typeMatches); foreach (var resource in created) @@ -169,7 +169,7 @@ private Resource UpdateSingleReference(ReferenceSaverContext context, ResourceEn throw new ValidationException($"Property {referenceProperty.Name} is flagged 'Required' and was null!"); // Check if there is a relation that represents this reference - if (referencedResource != null && matches.Any(m => referencedResource == context.ResolveReference(m))) + if (referencedResource != null && matches.Any(m => referencedResource == context.ResolveReferencedResource(m))) return null; // Get all references of this resource with the same relation information @@ -177,19 +177,19 @@ private Resource UpdateSingleReference(ReferenceSaverContext context, ResourceEn // Try to find a match that is not used in any reference var relMatch = (from match in matches - where currentReferences.All(cr => cr != context.ResolveReference(match)) + where currentReferences.All(cr => cr != context.ResolveReferencedResource(match)) select match).FirstOrDefault(); var relEntity = relMatch?.Entity; if (relEntity == null && referencedResource != null) { // Create a new relation relEntity = CreateRelationForProperty(context, relationRepo, referenceAtt); - SetOnTarget(referencedResource, resource, referenceAtt); + SetOnTarget(referencedResource, resource, referenceAtt, relEntity); } else if (relEntity != null && referencedResource == null) { // Delete a relation, that no longer exists - ClearOnTarget(context.ResolveReference(relMatch), resource, referenceAtt); + ClearOnTarget(context.ResolveReferencedResource(relMatch), resource, referenceAtt); relationRepo.Remove(relEntity); return null; } @@ -202,8 +202,8 @@ where currentReferences.All(cr => cr != context.ResolveReference(match)) // Relation was updated, make sure the backlinks match else { - ClearOnTarget(context.ResolveReference(relMatch), resource, referenceAtt); - SetOnTarget(referencedResource, resource, referenceAtt); + ClearOnTarget(context.ResolveReferencedResource(relMatch), resource, referenceAtt); + SetOnTarget(referencedResource, resource, referenceAtt, relEntity); } // Set source and target of the relation depending on the reference roles @@ -237,19 +237,19 @@ private IEnumerable UpdateCollectionReference(ReferenceSaverContext co // First delete references that are not used by ANY property of the same configuration var currentReferences = CurrentReferences(resource, referenceAtt); - var deleted = relationTemplates.Where(m => currentReferences.All(cr => cr != context.ResolveReference(m))).ToList(); + var deleted = relationTemplates.Where(m => currentReferences.All(cr => cr != context.ResolveReferencedResource(m))).ToList(); foreach (var relation in deleted) { - ClearOnTarget(context.ResolveReference(relation), resource, referenceAtt); + ClearOnTarget(context.ResolveReferencedResource(relation), resource, referenceAtt); relationRepo.Remove(relation.Entity); } // Now create new relations - var created = referencedResources.Where(rr => relationTemplates.All(m => rr != context.ResolveReference(m))).ToList(); + var created = referencedResources.Where(rr => relationTemplates.All(m => rr != context.ResolveReferencedResource(m))).ToList(); foreach (var createdReference in created) { - SetOnTarget(createdReference, resource, referenceAtt); var relEntity = CreateRelationForProperty(context, relationRepo, referenceAtt); + SetOnTarget(createdReference, resource, referenceAtt, relEntity); var referencedEntity = GetOrCreateEntity(context, createdReference); UpdateRelationEntity(entity, referencedEntity, relEntity, referenceAtt); } @@ -299,8 +299,7 @@ private static PropertyInfo FindBackLink(Resource target, Resource value, Resour var propOnTarget = (from prop in ReferenceProperties(target.GetType(), false) where IsInstanceOfReference(prop, value) let backAtt = prop.GetCustomAttribute() - where backAtt.Name == referenceAtt.Name // Compare name - && backAtt.RelationType == referenceAtt.RelationType // Compare relation type + where backAtt.RelationType == referenceAtt.RelationType // Compare relation type && backAtt.Role != referenceAtt.Role // Validate inverse role select prop).FirstOrDefault(); return propOnTarget; @@ -320,7 +319,7 @@ private static bool IsInstanceOfReference(PropertyInfo property, Resource value) /// /// Update backlink if possible /// - private static void SetOnTarget(Resource target, Resource value, ResourceReferenceAttribute referenceAtt) + private static void SetOnTarget(Resource target, Resource value, ResourceReferenceAttribute referenceAtt, ResourceRelationEntity relationEntity) { var prop = FindBackLink(target, value, referenceAtt); if (prop == null) @@ -330,13 +329,11 @@ private static void SetOnTarget(Resource target, Resource value, ResourceReferen { prop.SetValue(target, value); } - else - { - var collection = prop.GetValue(target) as IReferenceCollection; - if (collection != null && !collection.UnderlyingCollection.Contains(value)) - collection.UnderlyingCollection.Add(value); - } - + else if (prop.GetValue(target) is IReferenceCollection collection && !collection.UnderlyingCollection.Contains(value)) + collection.UnderlyingCollection.Add(value); + + var backAttr = prop.GetCustomAttribute(); + UpdateRelationEntity(relationEntity, backAttr); } /// @@ -404,18 +401,35 @@ private static void UpdateRelationEntity(ResourceEntity resource, ResourceEntity { if (att.Role == ResourceReferenceRole.Source) { + relEntity.SourceId = referencedResource.Id; relEntity.Source = referencedResource; + relEntity.TargetId = resource.Id; relEntity.Target = resource; relEntity.SourceName = att.Name; } else { relEntity.Source = resource; + relEntity.SourceId = resource.Id; relEntity.Target = referencedResource; + relEntity.TargetId = referencedResource.Id; relEntity.TargetName = att.Name; } } + /// + /// Set and depending on the + /// of the reference property + /// + private static void UpdateRelationEntity(ResourceRelationEntity relEntity, ResourceReferenceAttribute att) + { + if (att.Role == ResourceReferenceRole.Source) + relEntity.SourceName = att.Name; + else + relEntity.TargetName = att.Name; + } + + /// public void RemoveLinking(IResource deletedInstance, IResource reference) { @@ -477,8 +491,8 @@ public static IEnumerable TypeFilter(IEnumerable instanceResolver) { return from relation in relations - let target = instanceResolver(relation) - where IsInstanceOfReference(property, target) + let other = instanceResolver(relation) + where IsInstanceOfReference(property, other) select relation; } @@ -533,11 +547,11 @@ public ReferenceSaverContext(IUnitOfWork uow, IResourceGraph graph) public IList CreatedRelations { get; } /// - /// Resolve a relation reference + /// Resolve a referenced resource from a /// - public Resource ResolveReference(ResourceRelationAccessor relation) + public Resource ResolveReferencedResource(ResourceRelationAccessor refAccessor) { - return relation.ReferenceId > 0 ? _graph.Get(relation.ReferenceId) : ResourceLookup[relation.ReferenceEntity]; + return refAccessor.ReferenceId > 0 ? _graph.Get(refAccessor.ReferenceId) : ResourceLookup[refAccessor.ReferenceEntity]; } } } diff --git a/src/Tests/Moryx.Resources.Management.Tests/Mocks/ReferenceResource.cs b/src/Tests/Moryx.Resources.Management.Tests/Mocks/ReferenceResource.cs index 56f9d9150..cda0e92c4 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/Mocks/ReferenceResource.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/Mocks/ReferenceResource.cs @@ -67,6 +67,12 @@ public ISimpleResource Reference [ResourceReference(ResourceRelationType.CurrentExchangablePart)] public DerivedResource Reference2 { get; set; } + [ResourceReference(ResourceRelationType.Extension, ResourceReferenceRole.Target, nameof(TargetReference))] + public BidirectionalReferenceResource TargetReference { get; set; } + + [ResourceReference(ResourceRelationType.Extension, ResourceReferenceRole.Target, nameof(NewTargetReference))] + public BidirectionalReferenceResource NewTargetReference { get; set; } + [ResourceReference(ResourceRelationType.PossibleExchangablePart)] public IReferences References { get; set; } @@ -98,11 +104,16 @@ public void SetMany(IReadOnlyList references) References.Add(reference); } - public INonPublicResource NonPublic { get; set; } public event EventHandler ReferenceChanged; public event EventHandler SomeChanged; } + + public class BidirectionalReferenceResource : PublicResource + { + [ResourceReference(ResourceRelationType.Extension, ResourceReferenceRole.Source, nameof(SourceReference))] + public ReferenceResource SourceReference { get; set; } + } } diff --git a/src/Tests/Moryx.Resources.Management.Tests/ResourceLinkerTests.cs b/src/Tests/Moryx.Resources.Management.Tests/ResourceLinkerTests.cs index 81e026397..a6027ef37 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/ResourceLinkerTests.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/ResourceLinkerTests.cs @@ -127,6 +127,63 @@ private ResourceRelationAccessor RelationAccessor(long id, }; } + [Test(Description = "Save modified bidirectional references")] + public void SaveBidirectionalReferences() + { + // Arrange + var instance = new ReferenceResource { Id = 1 }; + ResourceReferenceTools.InitializeCollections(instance); + // Prepare reference objects + var resource = new BidirectionalReferenceResource { Id = 2, Name = "resource" }; + var collectionResource = new SimpleResource { Id = 3, Name = "collectionResource" }; + var newResource = new BidirectionalReferenceResource() { Name = "newResource" }; + ResourceReferenceTools.InitializeCollections(newResource); + var newCollectionResource = new SimpleResource() { Name = "newCollectionResource" }; + ResourceReferenceTools.InitializeCollections(newCollectionResource); + // Fill graph + _graph[1] = new ResourceWrapper(instance); + _graph[2] = new ResourceWrapper(resource); + _graph[3] = new ResourceWrapper(collectionResource); + // Set single references + instance.TargetReference = resource; // The bidirectional reference is created with an existing resource + instance.NewTargetReference = newResource; // The bidirectional reference is created with a new resource + // Fill collections + instance.ChildReferences.Add(collectionResource); // An existing resource is added to the reference collection + instance.ChildReferences.Add(newCollectionResource); // A new resource is added to the reference collection + // Setup uow and repo to simulate the current database + var relations = new List(); + var mocks = SetupDbMocks(relations); + + // Act + var newResources = _linker.SaveReferences(mocks.Item1.Object, instance, new ResourceEntity { Id = 1 }); + + // Assert + Assert.Multiple(() => + { + // Resources were created + Assert.DoesNotThrow(() => mocks.Item3.Verify(repo => repo.Create(), Times.Exactly(2)), "Linker did not detect the new resources"); + Assert.That(newResources, Has.Count.EqualTo(2)); + Assert.That(newResource, Is.EqualTo(newResources[0])); + Assert.That(newCollectionResource, Is.EqualTo(newResources[1])); + // Resources properties were set + Assert.That(resource.SourceReference, Is.EqualTo(instance), "Backlink sync failed for a reference to an existing resource"); + Assert.That(newResource.SourceReference, Is.EqualTo(instance), "Backlink sync failed for a reference to a new resource"); + Assert.That(collectionResource.Parent, Is.EqualTo(instance), "Backlink sync failed for collection reference to a new resource"); + Assert.That(newCollectionResource.Parent, Is.EqualTo(instance), "Backlink sync failed for a collection reference to a new resource"); + // Relations were created + Assert.DoesNotThrow(() => mocks.Item2.Verify(repo => repo.Create((int)ResourceRelationType.Extension), Times.Exactly(2)), "Linker did not create relations for references"); + Assert.DoesNotThrow(() => mocks.Item2.Verify(repo => repo.Create((int)ResourceRelationType.ParentChild), Times.Exactly(2)), "Linker did not create relations for collection references"); + Assert.That(relations.Where(r => r.RelationType == (int)ResourceRelationType.ParentChild).Count(), Is.EqualTo(2)); + Assert.That(relations.Where(r => r.RelationType == (int)ResourceRelationType.Extension).Count(), Is.EqualTo(2)); + // Relation sources and targets were set + Assert.That(relations.Where(r => r.Source.Id == instance.Id).Count(), Is.EqualTo(4)); + Assert.DoesNotThrow(() => relations.Single(r => r.Target.Id == resource.Id)); + Assert.DoesNotThrow(() => relations.Single(r => r.Target.Id == collectionResource.Id)); + Assert.DoesNotThrow(() => relations.Single(r => r.Target.Name == newResource.Name)); + Assert.DoesNotThrow(() => relations.Single(r => r.Target.Name == newCollectionResource.Name)); + }); + } + [Test(Description = "Save modified references of a resource")] public void SaveReferences() { @@ -200,7 +257,7 @@ public void SaveReferences() Assert.AreEqual(1, possiblePart.Count(r => r.Target.Id == 0)); } - [Test(Description = "Multiple references of the same relation type shoudl not interfere with each other")] + [Test(Description = "Multiple references of the same relation type should not interfere with each other")] public void ReferenceInterferenceOnSave() { // Arrange @@ -407,7 +464,7 @@ private static Tuple, Mock, Mock< var resRepo = new Mock(); resRepo.Setup(r => r.GetByKey(It.Is(id => id > 0))).Returns(id => new ResourceEntity { Id = id }); resRepo.Setup(r => r.GetByKey(It.Is(id => id == 0))).Returns((ResourceEntity)null); - resRepo.Setup(r => r.Create()).Returns(new ResourceEntity()); + resRepo.Setup(r => r.Create()).Returns(() => new ResourceEntity()); var uowMock = new Mock(); uowMock.Setup(u => u.GetRepository()).Returns(relRepo.Object); From dfa2bd440a20c695fdc8b7f244fe21590506cd8d Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Thu, 21 Nov 2024 14:27:44 +0100 Subject: [PATCH 72/82] Port: Fix exception on creating a bidirectional link to a new resource to release 8 --- .../Mocks/ReferenceResource.cs | 2 +- .../Moryx.Resources.Management.Tests/ResourceLinkerTests.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tests/Moryx.Resources.Management.Tests/Mocks/ReferenceResource.cs b/src/Tests/Moryx.Resources.Management.Tests/Mocks/ReferenceResource.cs index 50e0dc9c8..8ce461631 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/Mocks/ReferenceResource.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/Mocks/ReferenceResource.cs @@ -111,7 +111,7 @@ public void SetMany(IReadOnlyList references) public event EventHandler SomeChanged; } - public class BidirectionalReferenceResource : PublicResource + public class BidirectionalReferenceResource : Resource { [ResourceReference(ResourceRelationType.Extension, ResourceReferenceRole.Source, nameof(SourceReference))] public ReferenceResource SourceReference { get; set; } diff --git a/src/Tests/Moryx.Resources.Management.Tests/ResourceLinkerTests.cs b/src/Tests/Moryx.Resources.Management.Tests/ResourceLinkerTests.cs index 2a0e580c7..c068bf9ee 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/ResourceLinkerTests.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/ResourceLinkerTests.cs @@ -141,9 +141,9 @@ public void SaveBidirectionalReferences() var newCollectionResource = new SimpleResource() { Name = "newCollectionResource" }; ResourceReferenceTools.InitializeCollections(newCollectionResource); // Fill graph - _graph[1] = new ResourceWrapper(instance); - _graph[2] = new ResourceWrapper(resource); - _graph[3] = new ResourceWrapper(collectionResource); + _graph[1] = instance; + _graph[2] = resource; + _graph[3] = collectionResource; // Set single references instance.TargetReference = resource; // The bidirectional reference is created with an existing resource instance.NewTargetReference = newResource; // The bidirectional reference is created with a new resource From 4e8ff0ce24351bc4487b68f283e0537307559bae Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Mon, 25 Nov 2024 12:41:02 +0100 Subject: [PATCH 73/82] Navigate to `modules` by default It's more common to manage modules instead of databases, so that should be loaded by default. --- src/Moryx.CommandCenter.Web/src/common/container/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moryx.CommandCenter.Web/src/common/container/App.tsx b/src/Moryx.CommandCenter.Web/src/common/container/App.tsx index 1fe1e8c8a..dd81dd397 100644 --- a/src/Moryx.CommandCenter.Web/src/common/container/App.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/container/App.tsx @@ -104,7 +104,7 @@ function App(props: AppPropModel & AppDispatchPropModel) { } /> } /> - } /> + } /> From 7a374306d761af3fa7e957371dc9a6b6b2465ed2 Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Thu, 21 Nov 2024 16:29:41 +0100 Subject: [PATCH 74/82] Add `FunctionResult` to allow functions return 'proper' results Add generic `FunctionResult` that allows functions to always return a proper result, that either contains a valid value or an error and helps exercising error handling. --- src/Moryx/Tools/FunctionResult.cs | 246 +++++++++++++++ src/Tests/Moryx.Tests/Moryx.Tests.csproj | 1 + .../Tools/FunctionResultTestsBase.cs | 14 + .../Tools/FunctionResultWithNothingTests.cs | 171 ++++++++++ .../Tools/FunctionResultWithTypeTests.cs | 293 ++++++++++++++++++ 5 files changed, 725 insertions(+) create mode 100644 src/Moryx/Tools/FunctionResult.cs create mode 100644 src/Tests/Moryx.Tests/Tools/FunctionResultTestsBase.cs create mode 100644 src/Tests/Moryx.Tests/Tools/FunctionResultWithNothingTests.cs create mode 100644 src/Tests/Moryx.Tests/Tools/FunctionResultWithTypeTests.cs diff --git a/src/Moryx/Tools/FunctionResult.cs b/src/Moryx/Tools/FunctionResult.cs new file mode 100644 index 000000000..527691f29 --- /dev/null +++ b/src/Moryx/Tools/FunctionResult.cs @@ -0,0 +1,246 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System; + +namespace Moryx.Tools.FunctionResult +{ + /// + /// Generic type that allows functions to always return a proper result, + /// that either contains a valid value or an error and helps exercising + /// error handling. + /// + /// + public class FunctionResult + { + /// + /// Result value in case of success + /// + public TResult? Result { get; } = default; + + /// + /// Error in case of failure + /// + public FunctionResultError? Error { get; } = null; + + /// + /// Indicates if the result contains a valid value + /// or not + /// + public bool Success => Error == null; + + /// + /// Creates a result with a value + /// + /// + public FunctionResult(TResult result) + { + Result = result; + } + + /// + /// Creates an error result with + /// + /// + public FunctionResult(FunctionResultError error) + { + Error = error; + } + + /// + public override string ToString() + { + return Success + ? Result?.ToString() ?? "null" + : Error!.ToString(); + } + + /// + /// Process result value and errors in a 'pattern matching '-like way + /// + /// Function to be excecuted in case of success + /// Function to be excecuted in case of an error + /// of the executed function + public FunctionResult Match(Func> success, Func> error) + => Success ? success(Result!) : error(Error!); + + /// + /// Process result value and errors in a 'pattern matching '-like way + /// + /// Action to be excecuted in case of success + /// Action to be excecuted in case of an error + /// The current + public FunctionResult Match(Action success, Action error) + => Match( + s => + { + success(s); + return this; + }, + e => + { + error(e); + return this; + }); + } + + /// + /// of type to be used + /// for functions that would return + /// + public class FunctionResult : FunctionResult + { + /// + /// Creates a `successful` with 'no' value + /// + public FunctionResult() : base(new Nothing()) + { + } + + + /// + /// Creates an error result with + /// + /// + public FunctionResult(FunctionResultError error) : base(error) + { + } + + /// + /// Helper to create an Ok in a descriptive way. + /// + /// + public static FunctionResult Ok() + => new FunctionResult(); + + /// + /// Helper to create a with an error message in a descriptive way. + /// + /// + public static FunctionResult WithError(string message) + => new(new FunctionResultError(message)); + + /// + /// Helper to create a with an in a descriptive way. + /// + /// + public static FunctionResult WithError(Exception exception) + => new(new FunctionResultError(exception)); + + /// + /// Helper to create an Ok in a descriptive way. + /// + /// of + public static FunctionResult Ok(TResult result) + => new(result); + + /// + /// Helper to create a with an error message in a descriptive way. + /// + /// + public static FunctionResult WithError(string message) + => new(new FunctionResultError(message)); + + /// + /// Helper to create an Ok in a descriptive way. + /// + /// + public static FunctionResult WithError(Exception exception) + => new(new FunctionResultError(exception)); + + } + + /// + /// Holds a description of the error and optionally an + /// + public class FunctionResultError + { + /// + /// Error message + /// + public string Message { get; } + + /// + /// Exception that might be the reason for the error + /// + public Exception? Exception { get; } = null; + + + /// + /// Creates an error with error message + /// + /// In case of is null + public FunctionResultError(string message) + { + if (message is null) + throw new ArgumentNullException(nameof(message)); + + Message = message; + } + + /// + /// Creates an error with error message + /// + /// In case of is null + public FunctionResultError(Exception exception) + { + if (exception is null) + throw new ArgumentNullException(nameof(exception)); + + Message = exception.Message; + Exception = exception; + } + + /// + public override string ToString() + { + return Exception != null + ? Exception.Message + : Message; + } + + } + + + /// + /// Placeholder type to return nothing when for example + /// would be returned + /// + public class Nothing + { + } + + /// + /// Extensions for + /// + public static class FunctionResultExtensions + { + /// + /// Executes the provided function in case of a successful result + /// + /// returned by + public static FunctionResult Then(this FunctionResult result, Func> func) + => result.Match(func, _ => result); + + /// + /// Executes the provided function in case of an error result + /// + /// returned by + public static FunctionResult Catch(this FunctionResult result, Func> func) + => result.Match(_ => result, func); + + /// + /// Executes the provided action in case of a successful result + /// + /// The underlying + public static FunctionResult Then(this FunctionResult result, Action action) + => result.Match(action, _ => { }); + + /// + /// Executes the provided action in case of a error result + /// + /// The underlying + public static FunctionResult Catch(this FunctionResult result, Action action) + => result.Match(_ => { }, action); + } +} diff --git a/src/Tests/Moryx.Tests/Moryx.Tests.csproj b/src/Tests/Moryx.Tests/Moryx.Tests.csproj index ce8ad8b6e..16f9536dc 100644 --- a/src/Tests/Moryx.Tests/Moryx.Tests.csproj +++ b/src/Tests/Moryx.Tests/Moryx.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Tests/Moryx.Tests/Tools/FunctionResultTestsBase.cs b/src/Tests/Moryx.Tests/Tools/FunctionResultTestsBase.cs new file mode 100644 index 000000000..f79eec0f9 --- /dev/null +++ b/src/Tests/Moryx.Tests/Tools/FunctionResultTestsBase.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using NUnit.Framework; + +namespace Moryx.Tests.Tools +{ + [TestFixture] + public class FunctionResultTestsBase + { + protected const string Message = "Error occured!"; + protected const string ExceptionMessage = "Exception Message"; + } +} diff --git a/src/Tests/Moryx.Tests/Tools/FunctionResultWithNothingTests.cs b/src/Tests/Moryx.Tests/Tools/FunctionResultWithNothingTests.cs new file mode 100644 index 000000000..7057077cc --- /dev/null +++ b/src/Tests/Moryx.Tests/Tools/FunctionResultWithNothingTests.cs @@ -0,0 +1,171 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using NUnit.Framework; +using Moryx.Tools.FunctionResult; +using System; +using Moq; + +namespace Moryx.Tests.Tools; + +[TestFixture] +public class FunctionResultWithNothingTests : FunctionResultTestsBase +{ + protected Mock>> _funcMockSuccess; + protected Mock>> _funcMockError; + + protected Mock> _actionMockSuccess; + protected Mock> _actionMockError; + + [SetUp] + public void Setup() + { + _funcMockSuccess = new Mock>>(); + _funcMockSuccess + .Setup(f => f(It.IsAny())) + .Returns((Nothing arg) => new FunctionResult(arg)); + _funcMockError = new Mock>>(); + _funcMockError + .Setup(f => f(It.IsAny())) + .Returns((FunctionResultError arg) => new FunctionResult(arg)); + + _actionMockSuccess = new Mock>(); + _actionMockError = new Mock>(); + } + + + [Test] + public void ResultWithValueGetCreated() + { + var result = new FunctionResult(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Error, Is.Null); + Assert.That(result.Result, Is.TypeOf()); + Assert.That(result.ToString(), Contains.Substring("Nothing")); + } + + [Test] + public void ErrorResultWithMessageGetsCreated() + { + var result = new FunctionResult(new FunctionResultError(Message)); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.Null); + + Assert.That(result.Error.Message, Is.EqualTo(Message)); + Assert.That(result.Error.Exception, Is.Null); + Assert.That(result.ToString(), Is.EqualTo(Message)); + } + + [Test] + public void ErrorResultWithExceptionGetsCreated() + { + var result = new FunctionResult(new FunctionResultError(new Exception(ExceptionMessage))); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.Null); + + Assert.That(result.Error.Message, Is.EqualTo(ExceptionMessage)); + Assert.That(result.Error.Exception, Is.TypeOf()); + Assert.That(result.ToString(), Is.EqualTo(ExceptionMessage)); + } + + [Test] + public void ResultWithNothingGetsCreatedByUsingExtension() + { + var result = FunctionResult.Ok(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Result, Is.TypeOf()); + Assert.That(result.Error, Is.Null); + Assert.That(result.ToString(), Contains.Substring("Nothing")); + } + + [Test] + public void ErrorResultWithMessageGetsCreatedByUsingExtension() + { + var result = FunctionResult.WithError(Message); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.Null); + + + Assert.That(result.Error.Message, Is.EqualTo(Message)); + Assert.That(result.Error.Exception, Is.Null); + Assert.That(result.ToString(), Is.EqualTo(Message)); + } + + [Test] + public void ErrorResultWithExceptionGetsCreatedByUsingExtension() + { + FunctionResult result = FunctionResult.WithError(new Exception(ExceptionMessage)); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.Null); + + Assert.That(result.Error.Message, Is.EqualTo(ExceptionMessage)); + Assert.That(result.Error.Exception, Is.TypeOf()); + Assert.That(result.ToString(), Is.EqualTo(ExceptionMessage)); + } + + [Test] + public void CannotCreateErrorResultWithNullExeption() + { + Assert.Throws(() => { FunctionResult.WithError((Exception)null); }); + } + + [Test] + public void CannotCreateErrorResultWithNullMessage() + { + Assert.Throws(() => { FunctionResult.WithError((string)null); }); + } + + [Test] + public void ExecutesFuncOnError() + { + FunctionResult.WithError("500") + .Then(_funcMockSuccess.Object) + .Catch(_funcMockError.Object) + ; + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Never); + _funcMockError.Verify(f => f(It.IsAny()), Times.Once); + } + + [Test] + public void ExecutesFuncOnSuccess() + { + FunctionResult.Ok() + .Catch(_funcMockError.Object) + .Then(_funcMockSuccess.Object) + ; + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Once); + _funcMockError.Verify(f => f(It.IsAny()), Times.Never); + } + + [Test] + public void ExecutesActionOnError() + { + FunctionResult.WithError("500") + .Then(_actionMockSuccess.Object) + .Catch(_actionMockError.Object) + ; + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Never); + _actionMockError.Verify(a => a(It.IsAny()), Times.Once); + } + + [Test] + public void ExecutesActionOnSuccess() + { + FunctionResult.Ok() + .Catch(_actionMockError.Object) + .Then(_actionMockSuccess.Object) + ; + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Once); + _actionMockError.Verify(a => a(It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Moryx.Tests/Tools/FunctionResultWithTypeTests.cs b/src/Tests/Moryx.Tests/Tools/FunctionResultWithTypeTests.cs new file mode 100644 index 000000000..c84b6b755 --- /dev/null +++ b/src/Tests/Moryx.Tests/Tools/FunctionResultWithTypeTests.cs @@ -0,0 +1,293 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using NUnit.Framework; +using System; +using Moq; +using Moryx.Tools.FunctionResult; + +namespace Moryx.Tests.Tools; + +[TestFixture] +public class FunctionResultWithTypeTests : FunctionResultTestsBase +{ + protected Mock>> _funcMockSuccess; + protected Mock>> _funcMockError; + + protected Mock> _actionMockSuccess; + protected Mock> _actionMockError; + + [SetUp] + public void Setup() + { + _funcMockSuccess = new Mock>>(); + _funcMockSuccess + .Setup(f => f(It.IsAny())) + .Returns((int arg) => new FunctionResult(arg)); + _funcMockError = new Mock>>(); + _funcMockError + .Setup(f => f(It.IsAny())) + .Returns((FunctionResultError arg) => new FunctionResult(arg)); + + _actionMockSuccess = new Mock>(); + _actionMockError = new Mock>(); + } + + [Test] + public void ResultWithValueGetsCreated() + { + var result = new FunctionResult(1); + + Assert.That(result.Success, Is.True); + Assert.That(result.Error, Is.Null); + Assert.That(result.Result, Is.EqualTo(1)); + Assert.That(result.ToString(), Is.EqualTo("1")); + } + + [Test] + public void ErrorResultWithMessageGetsCreated() + { + var result = new FunctionResult(new FunctionResultError(Message)); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.EqualTo(0)); + + Assert.That(result.Error.Message, Is.EqualTo(Message)); + Assert.That(result.Error.Exception, Is.Null); + Assert.That(result.ToString(), Is.EqualTo(Message)); + } + + [Test] + public void ErrorResultWithExceptionGetsCreated() + { + var result = new FunctionResult(new FunctionResultError(new Exception(ExceptionMessage))); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.EqualTo(0)); + + Assert.That(result.Error.Message, Is.EqualTo(ExceptionMessage)); + Assert.That(result.Error.Exception, Is.TypeOf()); + Assert.That(result.ToString(), Is.EqualTo(ExceptionMessage)); + } + + [Test] + public void ResultWithValueGetsCreatedByUsingExtension() + { + var result = FunctionResult.Ok(10); + + Assert.That(result.Success, Is.True); + Assert.That(result.Result, Is.EqualTo(10)); + + Assert.That(result.Error, Is.Null); + Assert.That(result.ToString(), Is.EqualTo("10")); + } + + [Test] + public void ErrorResultWithMessageGetsCreatedByUsingExtension() + { + var result = FunctionResult.WithError(Message); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.EqualTo(0)); + + + Assert.That(result.Error.Message, Is.EqualTo(Message)); + Assert.That(result.Error.Exception, Is.Null); + Assert.That(result.ToString(), Is.EqualTo(Message)); + } + + [Test] + public void ErrorResultWithExceptionGetsCreatedByUsingExtension() + { + var result = FunctionResult.WithError(new Exception(ExceptionMessage)); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.EqualTo(0)); + + Assert.That(result.Error.Message, Is.EqualTo(ExceptionMessage)); + Assert.That(result.Error.Exception, Is.TypeOf()); + Assert.That(result.ToString(), Is.EqualTo(ExceptionMessage)); + } + + [Test] + public void ResultToStringEqualsTheResultsToStringReturnValue() + { + var floatResult = FunctionResult.Ok(3.14f); + string floatAsString = Convert.ToString(3.14f); // avoid localization issues + var noResult = FunctionResult.Ok(new Nothing()); + var nullResult = FunctionResult.Ok(null); + + Assert.Multiple(() => + { + Assert.That($"{floatResult}", Is.EqualTo(floatAsString)); + Assert.That($"{noResult}", Is.EqualTo(new Nothing().ToString())); + Assert.That($"{nullResult}", Is.EqualTo("null")); + }); + } + + [Test] + public void CannotCreateErrorResultWithNullExeption() + { + Assert.Throws(() => { FunctionResult.WithError((Exception)null); }); + } + + [Test] + public void CannotCreateErrorResultWithNullMessage() + { + Assert.Throws(() => { FunctionResult.WithError((string)null); }); + } + + [Test] + public void SuccessResultMatchesSuccess() + { + var result = FunctionResult.Ok(200); + + var matchResult = result + .Match( + success: _funcMockSuccess.Object, + error: _funcMockError.Object + ); + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Once); + _funcMockError.Verify(f => f(It.IsAny()), Times.Never); + + Assert.That(result, Is.Not.SameAs(matchResult)); + } + + + [Test] + public void ErrorResultMatchesErrorWithException() + { + var result = FunctionResult.WithError(new Exception("Internal server error")); + + var matchResult = result + .Match( + success: _funcMockSuccess.Object, + error: _funcMockError.Object + ); + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Never); + _funcMockError.Verify(f => f(It.IsAny()), Times.Once); + + // The assertion verifies, that the result can be different from the original + Assert.That(result, Is.Not.SameAs(matchResult)); + } + + [Test] + public void ErrorResultMatchesErrorWithMessage() + { + var result = FunctionResult.WithError("500"); + + var matchResult = result + .Match( + success: _funcMockSuccess.Object, + error: _funcMockError.Object + ); + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Never); + _funcMockError.Verify(f => f(It.IsAny()), Times.Once); + + // The assertion verifies, that the result can be different from the original + Assert.That(result, Is.Not.SameAs(matchResult)); + } + + [Test] + public void SuccessResultMatchesSuccessAction() + { + var result = FunctionResult.Ok(200); + + var matchResult = result + .Match( + success: _actionMockSuccess.Object, + error: _actionMockError.Object + ); + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Once); + _actionMockError.Verify(a => a(It.IsAny()), Times.Never); + + Assert.That(result, Is.SameAs(matchResult)); + } + + [Test] + public void ExceptionResultMatchesErrorAction() + { + var result = FunctionResult.WithError(new Exception("Internal server error")); + + var matchResult = result + .Match( + success: _actionMockSuccess.Object, + error: _actionMockError.Object + ); + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Never); + _actionMockError.Verify(a => a(It.IsAny()), Times.Once); + + Assert.That(result, Is.SameAs(matchResult)); + } + + [Test] + public void ErrorMessageResultMatchesErrorAction() + { + var result = FunctionResult.WithError("500"); + + var matchResult = result + .Match( + success: _actionMockSuccess.Object, + error: _actionMockError.Object + ); + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Never); + _actionMockError.Verify(a => a(It.IsAny()), Times.Once); + + Assert.That(result, Is.SameAs(matchResult)); + } + + [Test] + public void ExecutesFuncOnError() + { + FunctionResult.WithError("500") + .Then(_funcMockSuccess.Object) + .Catch(_funcMockError.Object) + ; + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Never); + _funcMockError.Verify(f => f(It.IsAny()), Times.Once); + } + + [Test] + public void ExecutesFuncOnSuccess() + { + FunctionResult.Ok(201) + .Catch(_funcMockError.Object) + .Then(_funcMockSuccess.Object) + ; + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Once); + _funcMockError.Verify(f => f(It.IsAny()), Times.Never); + } + + [Test] + public void ExecutesActionOnError() + { + FunctionResult.WithError("500") + .Then(_actionMockSuccess.Object) + .Catch(_actionMockError.Object) + ; + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Never); + _actionMockError.Verify(a => a(It.IsAny()), Times.Once); + } + + [Test] + public void ExecutesActionOnSuccess() + { + FunctionResult.Ok(42) + .Catch(_actionMockError.Object) + .Then(_actionMockSuccess.Object) + ; + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Once); + _actionMockError.Verify(a => a(It.IsAny()), Times.Never); + } + +} From 6d4d70e8fb3ad2e4b4df680618c444583a0cd46f Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Mon, 2 Dec 2024 14:21:28 +0100 Subject: [PATCH 75/82] Extend construct method for constructor invocations on resources Before, we created new resources using reflection and returned them always unsafed. This does not allow resources to act on the resource graph within their constructor methods, however. Hence, we added a conditional invocation to the resource modification interface, which creates a new resource in the resource graph with all required initializations. The behaviour of the endpoint is thereby extended with the option to create such resources with their respective constructors. --- .../ResourceManagementController.cs | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs index db75310fd..6959f84f7 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs @@ -18,6 +18,7 @@ using Moryx.Runtime.Container; using Moryx.Configuration; using System.Runtime.Serialization; +using System.ComponentModel.DataAnnotations; namespace Moryx.AbstractionLayer.Resources.Endpoints { @@ -33,8 +34,8 @@ public class ResourceModificationController : ControllerBase private readonly IResourceTypeTree _resourceTypeTree; private readonly ResourceSerialization _serialization; - public ResourceModificationController(IResourceManagement resourceManagement, - IResourceTypeTree resourceTypeTree, + public ResourceModificationController(IResourceManagement resourceManagement, + IResourceTypeTree resourceTypeTree, IModuleManager moduleManager, IServiceProvider serviceProvider) { @@ -123,7 +124,7 @@ public ActionResult InvokeMethod(long id, string method, Entry parameters return true; }); } - catch(MissingMethodException) + catch (MissingMethodException) { return BadRequest("Method could not be invoked. Please check spelling and access modifier (has to be `public` or `internal`)."); } @@ -144,30 +145,48 @@ public ActionResult InvokeMethod(long id, string method, Entry parameters public ActionResult ConstructWithParameters(string type, string method = null, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] Entry arguments = null) { var trustedType = WebUtility.HtmlEncode(type); - - return method == null ? Construct(trustedType, null) - : Construct(trustedType, new MethodEntry { Name = method, Parameters = arguments }); + if (method is null) + return Construct(trustedType); + else + return Construct(trustedType, new MethodEntry { Name = method, Parameters = arguments }); } - private ActionResult Construct(string type, MethodEntry method) + private ActionResult Construct(string type) { - var resource = (Resource)Activator.CreateInstance(_resourceTypeTree[type].ResourceType); - if (resource is null) + Resource resource; + try + { + resource = (Resource)Activator.CreateInstance(_resourceTypeTree[type].ResourceType); + } + catch (Exception) + { return NotFound(new MoryxExceptionResponse { Title = Strings.RESOURCE_NOT_FOUND }); + } ValueProviderExecutor.Execute(resource, new ValueProviderExecutorSettings() .AddFilter(new DataMemberAttributeValueProviderFilter(false)) .AddDefaultValueProvider()); - if (method != null) - EntryConvert.InvokeMethod(resource, method, _serialization); - var model = new ResourceToModelConverter(_resourceTypeTree, _serialization).GetDetails(resource); model.Methods = Array.Empty(); // Reset methods because they can not be invoked on new objects - return model; } + private ActionResult Construct(string type, MethodEntry method) + { + try + { + var id = _resourceManagement.Create(_resourceTypeTree[type].ResourceType, r => EntryConvert.InvokeMethod(r, method, _serialization)); + return GetDetails(id); + } + catch (Exception e) + { + if (e is ArgumentException or SerializationException or ValidationException) + return BadRequest(e.Message); + throw; + } + } + [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] @@ -187,11 +206,11 @@ public ActionResult Save(ResourceModel model) resourcesToSave.Skip(1).ForEach(id => _resourceManagement.Modify(id, r => true)); }); - return Ok(GetDetails(id)); + return GetDetails(id); } catch (Exception e) { - if (e is ArgumentException or SerializationException) + if (e is ArgumentException or SerializationException or ValidationException) return BadRequest(e.Message); throw; } @@ -325,7 +344,7 @@ private void UpdateReferences(Resource instance, HashSet resourcesToSave, [Authorize(Policy = ResourcePermissions.CanEdit)] public ActionResult Update(long id, ResourceModel model) { - if (_resourceManagement.GetAllResources(r=>r.Id == id) is null) + if (_resourceManagement.GetAllResources(r => r.Id == id) is null) return NotFound(new MoryxExceptionResponse { Title = string.Format(Strings.ResourceNotFoundException_ById_Message, id) }); try @@ -341,7 +360,7 @@ public ActionResult Update(long id, ResourceModel model) } catch (Exception e) { - if (e is ArgumentException or SerializationException) + if (e is ArgumentException or SerializationException or ValidationException) return BadRequest(e.Message); throw; } From 702a98d73528f7203f21dcbbbe740bdba2d6066b Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Thu, 5 Dec 2024 10:18:45 +0100 Subject: [PATCH 76/82] Add `ICameraDriver` Defines the interfaces for retrieving images from camera devices. Either directly or by registering for continuous streams. --- .../Drivers/Camera/ICameraDriver.cs | 31 +++++++++++++++++++ .../Drivers/Camera/ICameraDriverListener.cs | 18 +++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs create mode 100644 src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs diff --git a/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs new file mode 100644 index 000000000..3c4dd5653 --- /dev/null +++ b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs @@ -0,0 +1,31 @@ +using Moryx.AbstractionLayer.Drivers; +using System.Threading.Tasks; + +namespace Moryx.Drivers.Camera.Interfaces +{ + /// + /// Interface for camera devices, that provide image data + /// + public interface ICameraDriver : IDriver where TImage : class + { + /// + /// Registers an ICameraDriverListener that should be provided + /// with images. + /// + void Register(ICameraDriverListener listener); + + /// + /// Unregisters an ICameraDriverListener + /// + void Unregister(ICameraDriverListener listener); + + /// + /// Capture a single image from the camera + /// + /// + /// The image that was captured or null in case no image + /// could be retrieved + /// + Task CaptureImage(); + } +} diff --git a/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs new file mode 100644 index 000000000..da7a7fe34 --- /dev/null +++ b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs @@ -0,0 +1,18 @@ +using Moryx.AbstractionLayer.Drivers; +using System.Threading.Tasks; + +namespace Moryx.Drivers.Camera.Interfaces +{ + /// + /// Interface for objects that register as listeners to camera drivers + /// + public interface ICameraDriverListener where T : class + { + /// + /// Invoked, when a new image is received by the camera + /// + /// + /// + Task OnImage(T image); + } +} From 7c24221547bb8f068157339690a327e37f8deb52 Mon Sep 17 00:00:00 2001 From: Matho Camara Date: Fri, 6 Dec 2024 08:06:33 +0100 Subject: [PATCH 77/82] Allow Resource Management facade to notify when module is available --- .../Facades/ResourceManagementFacade.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs b/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs index 177ab9f06..c3db7f894 100644 --- a/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs +++ b/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs @@ -9,7 +9,7 @@ namespace Moryx.Resources.Management { - internal class ResourceManagementFacade : IResourceManagement, IFacadeControl + internal class ResourceManagementFacade : FacadeBase, IResourceManagement { #region Dependency Injection @@ -22,11 +22,9 @@ internal class ResourceManagementFacade : IResourceManagement, IFacadeControl #endregion #region IFacadeControl - /// - public Action ValidateHealthState { get; set; } /// - public void Activate() + public override void Activate() { Manager.ResourceAdded += OnResourceAdded; Manager.CapabilitiesChanged += OnCapabilitiesChanged; @@ -34,7 +32,7 @@ public void Activate() } /// - public void Deactivate() + public override void Deactivate() { Manager.ResourceAdded -= OnResourceAdded; Manager.CapabilitiesChanged -= OnCapabilitiesChanged; From ad3f9b5e95197dc2ad36e04852380dc56b1fa902 Mon Sep 17 00:00:00 2001 From: Matho Camara Date: Fri, 6 Dec 2024 08:06:33 +0100 Subject: [PATCH 78/82] Allow Resource Management facade to notify when module is available --- .../Facades/ResourceManagementFacade.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs b/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs index baa5f7a23..95cc8075d 100644 --- a/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs +++ b/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs @@ -9,7 +9,7 @@ namespace Moryx.Resources.Management { - internal class ResourceManagementFacade : IResourceManagement, IFacadeControl + internal class ResourceManagementFacade : FacadeBase, IResourceManagement { #region Dependency Injection @@ -22,11 +22,9 @@ internal class ResourceManagementFacade : IResourceManagement, IFacadeControl #endregion #region IFacadeControl - /// - public Action ValidateHealthState { get; set; } /// - public void Activate() + public override void Activate() { Manager.ResourceAdded += OnResourceAdded; Manager.CapabilitiesChanged += OnCapabilitiesChanged; @@ -34,7 +32,7 @@ public void Activate() } /// - public void Deactivate() + public override void Deactivate() { Manager.ResourceAdded -= OnResourceAdded; Manager.CapabilitiesChanged -= OnCapabilitiesChanged; From aaebfb7368ccb03ea42a323c93718076ffa2e089 Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Tue, 10 Dec 2024 14:12:15 +0100 Subject: [PATCH 79/82] Preset default file versions In order to properly include the projects in different solutions, e.g. to test changes in an application, we need to have the FileVersion set for the projects. --- .build/Common.props | 8 ++++---- MORYX-Framework.sln | 9 +++++---- .../Moryx.AbstractionLayer.Resources.Endpoints.csproj | 1 + src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj | 1 + src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj | 1 + src/Moryx.Container/Moryx.Container.csproj | 1 + src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj | 1 + src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj | 1 + src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj | 1 + src/Moryx.Model/Moryx.Model.csproj | 1 + src/Moryx.Notifications/Moryx.Notifications.csproj | 1 + .../Moryx.Resources.Management.csproj | 1 + src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj | 1 + src/Moryx.Runtime/Moryx.Runtime.csproj | 1 + src/Moryx/Moryx.csproj | 1 + 15 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.build/Common.props b/.build/Common.props index d5b059be3..c50b15d03 100644 --- a/.build/Common.props +++ b/.build/Common.props @@ -1,16 +1,16 @@ - 0.0.0 + 6.0.0 $(MORYX_ASSEMBLY_VERSION) - 0.0.0.0 + 6.0.0.0 $(MORYX_FILE_VERSION) - 0.0.0.0 + 6.0.0.0 $(MORYX_INFORMATIONAL_VERSION) - 0.0.0 + 6.0.0 $(MORYX_PACKAGE_VERSION) PHOENIXCONTACT diff --git a/MORYX-Framework.sln b/MORYX-Framework.sln index a62cf04a1..144390c6e 100644 --- a/MORYX-Framework.sln +++ b/MORYX-Framework.sln @@ -60,6 +60,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DFC092A6-B935-4D19-A564-9AEDEEF999B9}" ProjectSection(SolutionItems) = preProject Build.ps1 = Build.ps1 + .build\Common.props = .build\Common.props Directory.build.props = Directory.build.props Directory.Build.targets = Directory.Build.targets LICENSE = LICENSE @@ -118,9 +119,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.NUnit", "src\Moryx.TestTools.NUnit\Moryx.TestTools.NUnit.csproj", "{6FF878E0-AF61-4C3A-9B9C-71C35A949E51}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.TestTools.NUnit", "src\Moryx.TestTools.NUnit\Moryx.TestTools.NUnit.csproj", "{6FF878E0-AF61-4C3A-9B9C-71C35A949E51}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -356,8 +357,8 @@ Global {C949164C-0345-4893-9E4C-A79BC1F93F85} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - RESX_ShowErrorsInErrorList = True - RESX_TaskErrorCategory = Message SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243} + RESX_TaskErrorCategory = Message + RESX_ShowErrorsInErrorList = True EndGlobalSection EndGlobal diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj b/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj index 8c981b0b2..d83e545d2 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj @@ -6,6 +6,7 @@ REST Api in order to interact with the resource management true MORYX;IIoT;IoT;Resource + true diff --git a/src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj b/src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj index 6ce2a84f3..4dcbd846d 100644 --- a/src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj +++ b/src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj @@ -6,6 +6,7 @@ Domain model types and definitions of Cyber-physical systems in IIoT projects. true MORYX;IIoT;IoT;Hardware;Communication;Manufacturing;Industrial;Abstraction;Realtime + true diff --git a/src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj b/src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj index ea47f4474..465fb0f26 100644 --- a/src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj +++ b/src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj @@ -5,6 +5,7 @@ Extensions for the Integration of MORYX in ASP.NET Core true MORYX;ASP + true diff --git a/src/Moryx.Container/Moryx.Container.csproj b/src/Moryx.Container/Moryx.Container.csproj index d4aaf59c8..530083d7a 100644 --- a/src/Moryx.Container/Moryx.Container.csproj +++ b/src/Moryx.Container/Moryx.Container.csproj @@ -6,6 +6,7 @@ MORYX container functionality based on Castle.Windsor. true MORYX;Container;IoC + true diff --git a/src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj b/src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj index 7331a134a..106450304 100644 --- a/src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj +++ b/src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj @@ -6,6 +6,7 @@ InMemory extension for unit tests referencing Moryx.Model true MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database;InMemory;Effort + true diff --git a/src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj b/src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj index 19320c878..6bf832f3b 100644 --- a/src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj +++ b/src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj @@ -6,6 +6,7 @@ Adapter for Moryx.Model on PostgreSQL true MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database;Npgsql;PostgreSQL;Postgres + true diff --git a/src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj b/src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj index 416751019..32dcb0945 100644 --- a/src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj +++ b/src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj @@ -6,6 +6,7 @@ Adapter for Moryx.Model on Sqlite true MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database;Sqlite + true diff --git a/src/Moryx.Model/Moryx.Model.csproj b/src/Moryx.Model/Moryx.Model.csproj index 9b3e63695..a60ec2853 100644 --- a/src/Moryx.Model/Moryx.Model.csproj +++ b/src/Moryx.Model/Moryx.Model.csproj @@ -6,6 +6,7 @@ Datamodel integration for MORYX applications based on Entity Framework true MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database + true diff --git a/src/Moryx.Notifications/Moryx.Notifications.csproj b/src/Moryx.Notifications/Moryx.Notifications.csproj index 8a7719414..f90151d3b 100644 --- a/src/Moryx.Notifications/Moryx.Notifications.csproj +++ b/src/Moryx.Notifications/Moryx.Notifications.csproj @@ -6,6 +6,7 @@ Types and interfaces for notifications within MORYX applications true MORYX;IIoT;IoT;Notifications + true diff --git a/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj b/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj index fb46a416c..871672ac4 100644 --- a/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj +++ b/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj @@ -7,6 +7,7 @@ ResourceManagement module composing and maintaining the resource graph as the habitat for digital twins of manufacturing assets. true MORYX;IIoT;IoT;Manufacturing;API;Resource + true diff --git a/src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj b/src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj index fdc0e91d4..af0f71a82 100644 --- a/src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj +++ b/src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj @@ -6,6 +6,7 @@ Kernel components that comprise the MORYX server side runtime environment true MORYX;Kernel;Runtime;Server + true diff --git a/src/Moryx.Runtime/Moryx.Runtime.csproj b/src/Moryx.Runtime/Moryx.Runtime.csproj index 80aec6d44..af03381fa 100644 --- a/src/Moryx.Runtime/Moryx.Runtime.csproj +++ b/src/Moryx.Runtime/Moryx.Runtime.csproj @@ -6,6 +6,7 @@ Server side component types and implementations for MORYX applications true MORYX;Runtime;Server + true diff --git a/src/Moryx/Moryx.csproj b/src/Moryx/Moryx.csproj index 76d3188cd..42282fad1 100644 --- a/src/Moryx/Moryx.csproj +++ b/src/Moryx/Moryx.csproj @@ -7,6 +7,7 @@ Core package of the MORYX ecosystem. It defines the necessary types for MORYX compatibility as well as commonly required tools. true MORYX;Serialization;Configuration;Logging;Core;Modules;Workplans + true From 026c569ba0b49fbde370f71926fc297c84f5f5ce Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Tue, 10 Dec 2024 17:37:49 +0100 Subject: [PATCH 80/82] Filter generic base types when seaching for the resource proxy type When inheriting from a generic base type without adding additional interfaces, a resource can still have a generated proxy. However, in that case the proxy builder would assume the generic base type to be the best possible fit, which cannot be proxified. --- .../Resources/ResourceTypeController.cs | 8 +++-- .../Mocks/ResourceWithGenericMethod.cs | 32 ++++++++----------- .../TypeControllerTests.cs | 14 ++++++++ 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/Moryx.Resources.Management/Resources/ResourceTypeController.cs b/src/Moryx.Resources.Management/Resources/ResourceTypeController.cs index 893ef29a1..5140fb217 100644 --- a/src/Moryx.Resources.Management/Resources/ResourceTypeController.cs +++ b/src/Moryx.Resources.Management/Resources/ResourceTypeController.cs @@ -264,17 +264,19 @@ private ResourceProxy InstantiateProxy(string typeName, Resource instance) /// private void ProvideProxyType(Type resourceType) { - // Step 1: Find the least specific base type that offers the same amount of interfaces + // Step 1: Find the least specific base type that offers the same amount of interfaces and is not a generic itself // ReSharper disable once AssignNullToNotNullAttribute -> FullName should be not null var targetType = _typeCache[resourceType.ResourceType()]; var linker = targetType; var interfaces = RelevantInterfaces(linker); - // Move up the type tree until the parent offers less interfaces than the current linker - while (linker.BaseType != null && interfaces.Count == RelevantInterfaces(linker.BaseType).Count) + // Move up the type tree until the parent offers less interfaces than the current linker, is abstract or a generic + while (linker.BaseType != null && !linker.BaseType.ResourceType.IsGenericType + && interfaces.Count == RelevantInterfaces(linker.BaseType).Count) { linker = linker.BaseType; } + // Step 2: Check if we already created a proxy for this type. If we already // did use this one for the requested type as well. diff --git a/src/Tests/Moryx.Resources.Management.Tests/Mocks/ResourceWithGenericMethod.cs b/src/Tests/Moryx.Resources.Management.Tests/Mocks/ResourceWithGenericMethod.cs index ee751b561..07ef67d5c 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/Mocks/ResourceWithGenericMethod.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/Mocks/ResourceWithGenericMethod.cs @@ -37,24 +37,18 @@ public class ResourceWithGenericMethod : Resource, IGenericMethodCall, ISimpleRe public event EventHandler SomeEvent; public event EventHandler CapabilitiesChanged; - public IList GenericMethod(string identifier) - { - throw new NotImplementedException(); - } - - public int MultiplyFoo(int factor) - { - throw new NotImplementedException(); - } - - public int MultiplyFoo(int factor, ushort offset) - { - throw new NotImplementedException(); - } - - public void RaiseEvent() - { - throw new NotImplementedException(); - } + public IList GenericMethod(string identifier) => throw new NotImplementedException(); + + public int MultiplyFoo(int factor) => throw new NotImplementedException(); + + public int MultiplyFoo(int factor, ushort offset) => throw new NotImplementedException(); + + public void RaiseEvent() => throw new NotImplementedException(); } + + public interface IGenericBaseResourceInterface : IResource { } + + public class GenericBaseResource : Resource, IGenericBaseResourceInterface { } + + public class InheritingFromGenericResource : GenericBaseResource { } } diff --git a/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs b/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs index 8ef3f51f7..ccb8a8f9c 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs @@ -222,6 +222,20 @@ public void ProxyBuilderFiltersGenericInterfaces() Assert.IsFalse(typeof(IGenericMethodCall).IsAssignableFrom(proxy.GetType())); } + [Test] + public void ProxyBuilderSkipsGenericBaseTypes() + { + // Arrange + var driver = new InheritingFromGenericResource { Id = 42, Name = "A non generic resource inheriting from a generic base type" }; + + // Act + var proxy = _typeController.GetProxy(driver); + + // Assert + Assert.IsNotNull(proxy); + Assert.IsFalse(typeof(GenericBaseResource).IsAssignableFrom(proxy.GetType())); + } + [Test] public void FacadeExceptionForGenericProxy() { From 680b8d01ffad8eaf36258bc840a1535be02a74ab Mon Sep 17 00:00:00 2001 From: Marcel Vielhaus Date: Fri, 20 Dec 2024 10:17:31 +0100 Subject: [PATCH 81/82] Bump default file & package versions --- .build/Common.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.build/Common.props b/.build/Common.props index c50b15d03..efec47d4d 100644 --- a/.build/Common.props +++ b/.build/Common.props @@ -1,16 +1,16 @@ - 6.0.0 + 8.0.0 $(MORYX_ASSEMBLY_VERSION) - 6.0.0.0 + 8.0.0.0 $(MORYX_FILE_VERSION) - 6.0.0.0 + 8.0.0.0 $(MORYX_INFORMATIONAL_VERSION) - 6.0.0 + 8.0.0 $(MORYX_PACKAGE_VERSION) PHOENIXCONTACT From 5e150076351cb5c5ae810dceae1021f5b4e6cf3f Mon Sep 17 00:00:00 2001 From: Christian Siewert Date: Fri, 20 Dec 2024 11:01:27 +0100 Subject: [PATCH 82/82] Replace `ICameraDriverListener` with `EventHandler` --- .../Drivers/Camera/ICameraDriver.cs | 11 +++-------- .../Drivers/Camera/ICameraDriverListener.cs | 18 ------------------ 2 files changed, 3 insertions(+), 26 deletions(-) delete mode 100644 src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs diff --git a/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs index 3c4dd5653..9d287dc75 100644 --- a/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs +++ b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs @@ -1,4 +1,5 @@ using Moryx.AbstractionLayer.Drivers; +using System; using System.Threading.Tasks; namespace Moryx.Drivers.Camera.Interfaces @@ -9,15 +10,9 @@ namespace Moryx.Drivers.Camera.Interfaces public interface ICameraDriver : IDriver where TImage : class { /// - /// Registers an ICameraDriverListener that should be provided - /// with images. + /// Eventhandler to continously provide images from a camera /// - void Register(ICameraDriverListener listener); - - /// - /// Unregisters an ICameraDriverListener - /// - void Unregister(ICameraDriverListener listener); + event EventHandler CapturedImage; /// /// Capture a single image from the camera diff --git a/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs deleted file mode 100644 index da7a7fe34..000000000 --- a/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Moryx.AbstractionLayer.Drivers; -using System.Threading.Tasks; - -namespace Moryx.Drivers.Camera.Interfaces -{ - /// - /// Interface for objects that register as listeners to camera drivers - /// - public interface ICameraDriverListener where T : class - { - /// - /// Invoked, when a new image is received by the camera - /// - /// - /// - Task OnImage(T image); - } -}