diff --git a/README.md b/README.md index 78988bb..56ab800 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,250 @@ -# Virto Commerce Environments Compare Module +# Environments Compare Module ## Overview -Virto Commerce Environments Comparison Module allows backend users to compare platform and store settings across multiple environments -## Key features +The Environments Compare Module enables backend administrators to compare platform settings, environment configurations, and system information across multiple Virto Commerce environments (development, staging, production, etc.). This module helps identify configuration discrepancies, troubleshoot environment-specific issues, and ensure consistency across deployments. + +The module provides a web-based interface within the Virto Commerce platform where users can select multiple environments, compare their settings side-by-side, and view differences with visual indicators. It supports secure communication with remote environments using API key authentication and automatically masks sensitive data such as passwords and secure strings. + +## Key Features + +- **Multi-Environment Comparison**: Compare settings across two or more environments simultaneously, including the current environment and remote environments +- **Comprehensive Settings Coverage**: + - Platform settings (grouped by settings groups) + - Environment variables (system and process variables) + - .NET runtime information (framework version, OS description, architecture) + - Server features and hosting configuration +- **Environment Settings View**: From the environments list, click any environment to open a dedicated blade showing all of its settings in the same structured, filterable layout as the comparison view. This blade focuses on a single environment (without comparison controls), making it easy to inspect, search, and review configuration scopes and groups for one environment at a time. +- **Base Environment Comparison**: Select a base environment and compare all other environments against it to highlight differences +- **Difference Filtering**: Toggle between showing all settings or only differences to focus on what's changed +- **Search**: Quickly filter specific settings using a search bar +- **Settings Export**: Choose an environment and export its settings for future reference +- **Security Features**: + - API key-based authentication for secure communication with remote environments + - Automatic masking of secret/sensitive settings (passwords, secure strings) using SHA1 hashes +- **Visual Indicators**: Clear visual feedback showing which settings differ from the base environment and which environments have errors +- **Error Handling**: Displays connection errors and missing settings with descriptive error messages +- **External API Endpoint**: Provides a secure endpoint for remote environments to expose their settings for comparison ## Screenshots +### Admin UI +image + +### All Environments are Identical +image + +### Filter +image + +### Connection Error +image + +## Setup + +### Prerequisites + +Before configuring the Environments Compare module, ensure you have: + +- At least two running Virto Commerce environments with the Environments Compare module installed on each +- The main environment (from which comparisons are performed) has network access to secondary environments via HTTP/HTTPS protocol +- On each secondary environment: + - Create **Environments Compare** role with **environments-compare:read** permission + - Create an account assigned to the EnvironmentsCompare role + - Create API keys for user +- Valid URLs for all secondary environments that will be compared + +### Configuration + +#### Main Environment Configuration + +> Note: We recommend to keep ApiKey and URL in a secure storage for security reasons. + +On the main environment (the environment from which you will perform comparisons), configure the list of secondary environments to compare. Add the following configuration to your `appSettings.json`: + +```json +{ + "EnvironmentsCompare": { + "CurrentEnvironmentName": "Production", + "ComparableEnvironments": [ + { + "Name": "QA", + "Url": "https://qa.mydomaim.com", + "ApiKey": "a4a86441-cabb-4a60-af90-9c6ebe11a401" + }, + { + "Name": "Development", + "Url": "https://dev.mydomaim.com", + "ApiKey": "a4a86441-cabb-4a60-af90-9c6ebe11a401" + } + ] + } +} +``` + +For deployment configuration files (`.yml` format), use the following structure: + +```yaml +EnvironmentsCompare__CurrentEnvironmentName=Production +EnvironmentsCompare__ComparableEnvironments__0__Name: QA +EnvironmentsCompare__ComparableEnvironments__0__Url: https://qa.mydomaim.com +EnvironmentsCompare__ComparableEnvironments__0__ApiKey: a4a86441-cabb-4a60-af90-9c6ebe11a401 +EnvironmentsCompare__ComparableEnvironments__1__Name: Development +EnvironmentsCompare__ComparableEnvironments__1__Url: https://dev.mydomaim.com +EnvironmentsCompare__ComparableEnvironments__1__ApiKey: a4a86441-cabb-4a60-af90-9c6ebe11a401 +``` + +**Configuration Parameters:** +- `Name`: A descriptive name for the environment (e.g., "Staging", "Production", "Development") +- `Url`: The base URL of the secondary environment (must be accessible from the main environment) +- `ApiKey`: The ApiKey authentication for user in Virto Commerce platform on the secondary environment + +#### Secondary Environment Configuration + +On each secondary environment that will be compared: +1. Configure role (ex. named: `Environment Compare`) with `environments-compare:read` permission +1. Create a new user with this role +1. Create API key for this user + +### Configuration Whitelist + +By default, the module has a whitelist of appsettings sections and keys that are compared across environments. + +You can extend or narrow down which appsettings sections and keys are compared by configuring `EnvironmentsCompare:WhiteList`. +It supports `Include` and `Exclude` lists for both `SectionKeys` and `SettingKeys`. + +- `SectionKeys` controls which top-level configuration sections are considered. +- `SettingKeys` controls which specific keys are treated as public (non-secret) during comparison. + +Example `appsettings.json`: + +```json +{ + "EnvironmentsCompare": { + "WhiteList": { + "SectionKeys": { + "Include": [ "Logging", "ConnectionStrings" ], + "Exclude": [ "Notifications" ] + }, + "SettingKeys": { + "Include": [ "LoginPageUI:BackgroundUrl" ], + "Exclude": [ "Assets:AzureBlobStorage:CdnUrl" ] + } + } + } +} +``` + +## Scenarios + +### Accessing the Environments List + +1. Open the main environment in your browser +2. Navigate to the Environments Compare module +3. The environments list displays: + - **Current** environment (the main environment, always available) + - All secondary environments configured in the `ComparableEnvironments` setting + +### Exporting Environment Settings + +You can export settings from any environment for documentation or backup purposes: + +1. From the environments list, select the environment you want to export +2. Click the export action for that environment +3. The system generates a JSON file containing all settings for the selected environment + +**Export Behavior:** +- **Current Environment**: Exports a comprehensive JSON file with all available settings +- **Secondary Environments**: Exports settings retrieved from the remote environment. If the connection fails, the exported file will contain an error description instead of settings + +### Comparing Environments + +1. From the environments list, select two or more environments to compare (including the current environment if desired) +2. Click the **"Compare"** button +3. The comparison blade opens, displaying settings side-by-side in columns + +**Comparison Interface Features:** + +- **Environment Status Indicators:** + - Environments that failed to connect are displayed in gray + - The current environment (if selected) is always visible and available + - Error messages are displayed for environments with connection issues + +- **Base Environment Selection:** + - The leftmost environment column is used as the base for comparison by default + - To change the base environment, click the column header with the microscope icon (🔬) + - Selecting an unavailable environment as the base will result in all settings being marked as different + +- **View Modes:** + - **Show Differences Only** (default): Displays only settings that differ from the base environment + - **Show All**: Toggle to view all settings, including those that match the base environment + +- **Visual Indicators:** + - Settings with differences are highlighted + - Setting scopes (e.g., AppSettings, PlatformSettings, StoreSettings) have colored left borders to help identify them when scrolling + - Color coding helps distinguish between different setting groups + +### Extending with a custom IComparableSettingsProvider + +Use a custom provider when you need to: +- Compare additional configuration sources not covered by the built-in providers (e.g., external services, secrets vaults, tenant-specific configs). +- Add domain/module-specific settings scopes and grouping rules. +- Normalize or transform values before comparison (e.g., redact parts of secrets, map legacy keys, or compute derived values). + +Providers are responsible for returning one or more `ComparableSettingScope` objects containing groups and settings ready for the comparison UI. + +Minimal implementation: + +```csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; + +public class MyCustomSettingsProvider : IComparableSettingsProvider +{ + public Task> GetComparableSettingsAsync() { var scope = AbstractTypeFactory.TryCreateInstance(); scope.ScopeName = "MyCustomScope"; + var group = AbstractTypeFactory.TryCreateInstance(); + group.GroupName = "MyCustomGroup"; + scope.SettingGroups.Add(group); + + // Example settings (IsSecret = false marks it public for direct comparison) + var s1 = AbstractTypeFactory.TryCreateInstance(); + s1.Name = "MySection:MyKey"; + s1.Value = "SomeValue"; + s1.IsSecret = false; + group.Settings.Add(s1); + + var s2 = AbstractTypeFactory.TryCreateInstance(); + s2.Name = "MySection:SecretKey"; + s2.Value = "redacted"; // or hashed + s2.IsSecret = true; // will be compared as secure + group.Settings.Add(s2); + + return Task.FromResult>(new List { scope }); + } +} +``` + +Register your provider in `Module.cs` (Web project), register the provider in DI so it participates in comparison. + +```csharp +serviceCollection.AddTransient(); +``` + + ## References -* [Deployment](https://docs.virtocommerce.org/platform/developer-guide/Tutorials-and-How-tos/Tutorials/deploy-module-from-source-code/) -* [Installation](https://docs.virtocommerce.org/platform/user-guide/modules-installation/) -* [Home](https://virtocommerce.com) -* [Community](https://www.virtocommerce.org) -* [Download latest release](https://github.com/VirtoCommerce/vc-module-push-messages/releases) + +- [Deployment](https://docs.virtocommerce.org/platform/developer-guide/Tutorials-and-How-tos/Tutorials/deploy-module-from-source-code/) +- [Installation](https://docs.virtocommerce.org/platform/user-guide/modules-installation/) +- [Home](https://virtocommerce.com) +- [Community](https://www.virtocommerce.org) +- [Download latest release](https://github.com/VirtoCommerce/vc-module-environments-compare/releases) ## License -Copyright (c) Virto Solutions LTD. All rights reserved. + +Copyright (c) Virto Solutions LTD. All rights reserved. This software is licensed under the Virto Commerce Open Software License (the "License"); you may not use this file except in compliance with the License. You may diff --git a/docs/media/diagram-db-model.drawio b/docs/media/diagram-db-model.drawio deleted file mode 100644 index 7d64fff..0000000 --- a/docs/media/diagram-db-model.drawio +++ /dev/null @@ -1 +0,0 @@ -7Vtdc5s4FP01zOw+NMOHsZ3HmNrtTO1tJkl3H3cUkEFTjBghf/XXr4QkMJadQLeO1dgzngQuF4HuOTqSThzLCxabTwTkyQxHMLVcO9pY3kfLdR3bG7JfPLIVkb49EIGYoEgm1YFH9AOqO2V0iSJYNBIpxilFeTMY4iyDIW3EACF43Uyb47T51BzEUAs8hiDVo/+giCYiOvTtOv4ZojhRT3ZseWUBVLIMFAmI8LoRghs6wRmVr3gPyQJkMKPsygyQ75BY/jihlPf0znIn7DPn2TcxxnEKQY6KmxAvWDgsWMpkDhYo5WXeaWgkG2KP88aWFxCMqThabAKYcqwUDOKdJkeuVnUgvN0WNwQfVtvpd5tmn9fe33Q2i+8Hzx9kKyuQLmV9ZW3oVhUcRqz+8pQ9CtHtA0wBRTgb11dGMIvuOLosafzwAxL8hGcgY50fFRQQWl/DmUyfIPZ63kdHpchzm53rXZPvWeAlCeEL/XF7kpKAxJC+kNiXgEcNtsnKfYJ4ASnZsoR1zbGKSckuv1SQlEVZNVkKJJPiqsHqGfcYlcSSA9PryXbksBwoSqomRNflXbsw7zXU22uov9+QKI3WEDvY6XgdKlnUhVHuO6OU15JRzsAwSvXsJhOc4c9Sqt9syLX9N6WUzqivJIKk0HjFFD3nhxQ8lwwqSSCnMI+zgE1KFKCM6a9gSYjTFOQFKtNFJEFpNAVbvKSqIXU2mqMNjB7EDMZzGf+mrLFCUozPBYrV/DJIUZyx45BRjj9xRGDB3mUKCiozjpJyBQmFmxdZpLDx90BWw32HZa5/iGWefZxQDQS7wuW9LgAKKNZ1ikD6wBYKIItLzJqQ8LpGBOdPavTxQM6JBsl4BcVUXRafDfUAp5gjmwlBKNPKzvkj9mHdDewb3/LZCwTs3KnP2YenExrgrKAEoBIOyIBaQw7WiOJcPieFc/UaRBaTHz9jStnMfwzWF2n9OtYSW68ltCdDtqche/+lC7aY9XWelqqdoCiCmRiSfJ0IarwPQHmw/lXN98HYH4gt8fBa47EDgPeW9fe1+mMuhP+ymwLXurOFav/19Yn//DadWtVS1yR4lC6K3FGRgxBl8VTc2d/Dzz8Ffhvr6Hhy3xLP/iUr5eFV2a9Ryv65lXKgITv54nQB943GYksA+q0BOJc0DrWCh8uC9Vmoo3tAGo3D4lVd/OVgmaKDt1cdPI0O3p5bB5Vr+JPQmqWCt63Lfy4VdHQ/T6wQI0Ahi8tfFyGD7dEyRQYdfef8mKB8UYrW1evQvA532FLeTud1OPqW+IKmrg6ba6f97swUt8PRt9vvye6oqGvwdKZvkAspiFfPoyuIxsxy+t74giSzw3Kzu2Se3fZw9G34b+17VFw1WCP1zXNlCl+U7dEBK1Ok0P1/u+OrFBrsfLj6VtzAwdey/hVRzdVBVd5Da8ULdD86IGaMGur2RyA9/Kv9Ye18taPzVz3sk0F2tT9aDsffz/5w37f94Zpvf7i6/fHaHzUvyfvogKAxU9zV+ziVXp7d+3B178PAwdi2/uYbH65ufFTqmIEFX+mHCSB/+PafF7PoN8kCYaf1P7eIr4LX/5Hkjf8D \ No newline at end of file diff --git a/docs/media/diagram-db-model.png b/docs/media/diagram-db-model.png deleted file mode 100644 index 8346629..0000000 Binary files a/docs/media/diagram-db-model.png and /dev/null differ diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/.keep b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableEnvironment.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableEnvironment.cs new file mode 100644 index 0000000..f092242 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableEnvironment.cs @@ -0,0 +1,10 @@ +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparableEnvironment +{ + public string Name { get; set; } + + public string Url { get; set; } + + public string ApiKey { get; set; } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableEnvironmentSettings.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableEnvironmentSettings.cs new file mode 100644 index 0000000..dbabfba --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableEnvironmentSettings.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparableEnvironmentSettings +{ + public bool IsCurrent { get; set; } + + public string EnvironmentName { get; set; } + + public IList SettingScopes { get; set; } = []; + + public string ErrorMessage { get; set; } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSetting.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSetting.cs new file mode 100644 index 0000000..d6acf7f --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSetting.cs @@ -0,0 +1,12 @@ +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparableSetting +{ + public string Name { get; set; } + public string Description { get; set; } + + public object Value { get; set; } + + public bool IsSecret { get; set; } + +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSettingGroup.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSettingGroup.cs new file mode 100644 index 0000000..5a5b656 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSettingGroup.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparableSettingGroup +{ + public string GroupName { get; set; } + + public IList Settings { get; set; } = []; +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSettingScope.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSettingScope.cs new file mode 100644 index 0000000..4243387 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparableSettingScope.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparableSettingScope +{ + /// + /// Scope of the settings, e.g. "Platform", "AppSettings" + /// + public string ScopeName { get; set; } + + public string ProviderName { get; set; } + + public string ErrorMessage { get; set; } + + public IList SettingGroups { get; set; } = []; +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironment.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironment.cs new file mode 100644 index 0000000..966331e --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironment.cs @@ -0,0 +1,12 @@ +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparedEnvironment +{ + public bool IsComparisonBase { get; set; } + + public bool IsCurrent { get; set; } + + public string EnvironmentName { get; set; } + + public string ErrorMessage { get; set; } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSetting.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSetting.cs new file mode 100644 index 0000000..b130836 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSetting.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparedEnvironmentSetting +{ + public string Name { get; set; } + + public IList ComparedValues { get; set; } = []; +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingGroup.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingGroup.cs new file mode 100644 index 0000000..de31e4c --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingGroup.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparedEnvironmentSettingGroup +{ + public string GroupName { get; set; } + + public IList Settings { get; set; } = []; +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingScope.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingScope.cs new file mode 100644 index 0000000..0720845 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingScope.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparedEnvironmentSettingScope +{ + public string ScopeName { get; set; } + + public IList SettingGroups { get; set; } = []; +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingValue.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingValue.cs new file mode 100644 index 0000000..8c0e8cc --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/ComparedEnvironmentSettingValue.cs @@ -0,0 +1,12 @@ +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class ComparedEnvironmentSettingValue +{ + public string EnvironmentName { get; set; } + + public object Value { get; set; } + + public bool? EqualsBaseValue { get; set; } + + public string ErrorMessage { get; set; } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/EnvironmentsCompareSettings.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/EnvironmentsCompareSettings.cs new file mode 100644 index 0000000..f267ba9 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/EnvironmentsCompareSettings.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; +public class EnvironmentsCompareSettings +{ + public string CurrentEnvironmentName { get; set; } = ModuleConstants.EnvironmentsCompare.CurrentEnvironmentName; + + public IList ComparableEnvironments { get; set; } = []; + + public WhiteListSettings WhiteList { get; set; } = new WhiteListSettings(); +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/SettingsComparisonResult.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/SettingsComparisonResult.cs new file mode 100644 index 0000000..936d0fd --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/SettingsComparisonResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class SettingsComparisonResult +{ + public IList ComparedEnvironments { get; set; } = []; + + public IList SettingScopes { get; set; } = []; +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Models/WhiteListSettings.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/WhiteListSettings.cs new file mode 100644 index 0000000..6e8dcb8 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Models/WhiteListSettings.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Models; + +public class WhiteListSettings +{ + public WhiteListSetting SectionKeys { get; set; } = new(); + public WhiteListSetting SettingKeys { get; set; } = new(); +} + +public class WhiteListSetting +{ + public IList Include { get; set; } = []; + public IList Exclude { get; set; } = []; +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/ModuleConstants.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/ModuleConstants.cs index e0a0d05..9e0529c 100644 --- a/src/VirtoCommerce.EnvironmentsCompare.Core/ModuleConstants.cs +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/ModuleConstants.cs @@ -10,19 +10,9 @@ public static class Security public static class Permissions { public const string Access = "environments-compare:access"; - public const string Create = "environments-compare:create"; public const string Read = "environments-compare:read"; - public const string Update = "environments-compare:update"; - public const string Delete = "environments-compare:delete"; - public static string[] AllPermissions { get; } = - [ - Access, - Create, - Read, - Update, - Delete, - ]; + public static string[] AllPermissions { get; } = [Access, Read]; } } @@ -30,19 +20,11 @@ public static class Settings { public static class General { - public static SettingDescriptor EnvironmentsCompareEnabled { get; } = new() - { - Name = "EnvironmentsCompare.Enabled", - GroupName = "EnvironmentsCompare|General", - ValueType = SettingValueType.Boolean, - DefaultValue = false, - }; - public static IEnumerable AllGeneralSettings { get { - yield return EnvironmentsCompareEnabled; + return []; } } } @@ -55,4 +37,12 @@ public static IEnumerable AllSettings } } } + + public static class EnvironmentsCompare + { + public const string CurrentEnvironmentName = "Current"; + + public const string ApiAuthorizationKeyHeaderName = "api_key"; + public const string ApiEnvironmentsCompareRoute = "api/environments-compare-external"; + } } diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Services/.keep b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IComparableSettingsMasterProvider.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IComparableSettingsMasterProvider.cs new file mode 100644 index 0000000..e3f3cb9 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IComparableSettingsMasterProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Services; + +public interface IComparableSettingsMasterProvider +{ + Task> GetAllComparableSettingsAsync(); +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IComparableSettingsProvider.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IComparableSettingsProvider.cs new file mode 100644 index 0000000..6ad0add --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IComparableSettingsProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Services; + +public interface IComparableSettingsProvider +{ + Task> GetComparableSettingsAsync(); +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IEnvironmentsCompareClient.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IEnvironmentsCompareClient.cs new file mode 100644 index 0000000..12c7d3d --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IEnvironmentsCompareClient.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Services; + +public interface IEnvironmentsCompareClient +{ + Task> GetSettingsAsync(IList comparableEnvironments); +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IEnvironmentsCompareService.cs b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IEnvironmentsCompareService.cs new file mode 100644 index 0000000..1dfb610 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Core/Services/IEnvironmentsCompareService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; + +namespace VirtoCommerce.EnvironmentsCompare.Core.Services; + +public interface IEnvironmentsCompareService +{ + Task CompareAsync(IList environmentNames, string baseEnvironmentName = null, bool showAll = false); + + Task> GetComparableEnvironmentsAsync(IList environmentNames); +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/.keep b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableAppSettingsProvider.cs b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableAppSettingsProvider.cs new file mode 100644 index 0000000..dd20c1c --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableAppSettingsProvider.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.EnvironmentsCompare.Data.Services; + +public class ComparableAppSettingsProvider(IConfiguration configuration, IOptions options) : IComparableSettingsProvider +{ + protected virtual IList VisibleSectionKeys + { + get + { + var defaults = new List + { + "DatabaseProvider", + "ConnectionStrings", + "SqlServer", + "Serilog", + "FrontendSecurity", + "VirtoCommerce", + "Auth", + "Assets", + "Notifications", + "IdentityOptions", + "ExternalModules", + "Search", + "Content", + "Authorization", + "SecurityHeaders", + "AzureAd", + "Caching", + "Crud", + "PushNotifications", + "LoginPageUI", + "DefaultMainMenuState", + }; + + return MergeWithConfiguration(defaults, + options.Value?.WhiteList?.SectionKeys?.Include ?? [], + options.Value?.WhiteList?.SectionKeys?.Exclude ?? []); + } + } + + protected virtual IList PublicSettingKeys + { + get + { + var defaults = new List + { + "Assets:AzureBlobStorage:CdnUrl", + "Assets:FileSystem:PublicUrl", + "Assets:FileSystem:RootPath", + "Assets:Provider", + "Auth:Audience", + "Auth:Authority", + "Auth:PrivateKeyPath", + "Auth:PublicCertPath", + "Authorization:AccessTokenLifeTime", + "Authorization:AllowApiAccessForCustomers", + "Authorization:LimitedCookiePermissions", + "Authorization:RefreshTokenLifeTime", + "Authorization:ReturnPasswordHash", + "AzureAd:ApplicationId", + "AzureAd:AuthenticationCaption", + "AzureAd:AuthenticationType", + "AzureAd:AzureAdInstance", + "AzureAd:DefaultUserType", + "AzureAd:Enabled", + "AzureAd:TenantId", + "AzureAd:UsePreferredUsername", + "Caching:CacheEnabled", + "Caching:CacheSlidingExpiration", + "Caching:Redis:ChannelName", + "Content:AzureBlobStorage:CdnUrl", + "Content:AzureBlobStorage:RootPath", + "Content:FileSystem:PublicUrl", + "Content:FileSystem:RootPath", + "Content:Provider", + "Crud:MaxResultWindow", + "DatabaseProvider", + "DefaultMainMenuState", + "DefaultMainMenuState:items:0:isFavorite", + "DefaultMainMenuState:items:0:order", + "DefaultMainMenuState:items:0:path", + "DefaultMainMenuState:items:1:isFavorite", + "DefaultMainMenuState:items:1:order", + "DefaultMainMenuState:items:1:path", + "DefaultMainMenuState:items:2:isFavorite", + "DefaultMainMenuState:items:2:order", + "DefaultMainMenuState:items:2:path", + "DefaultMainMenuState:items:3:isFavorite", + "DefaultMainMenuState:items:3:order", + "DefaultMainMenuState:items:3:path", + "DefaultMainMenuState:items:4:isFavorite", + "DefaultMainMenuState:items:4:order", + "DefaultMainMenuState:items:4:path", + "DefaultMainMenuState:items:5:isFavorite", + "DefaultMainMenuState:items:5:order", + "DefaultMainMenuState:items:5:path", + "DefaultMainMenuState:items:6:isFavorite", + "DefaultMainMenuState:items:6:order", + "DefaultMainMenuState:items:6:path", + "DefaultMainMenuState:items:7:isFavorite", + "DefaultMainMenuState:items:7:order", + "DefaultMainMenuState:items:7:path", + "DefaultMainMenuState:items:8:isFavorite", + "DefaultMainMenuState:items:8:order", + "DefaultMainMenuState:items:8:path", + "ExternalModules:AuthorizationToken", + "ExternalModules:AutoInstallModuleBundles:0", + "ExternalModules:IncludePrerelease", + "ExternalModules:ModulesManifestUrl", + "FrontendSecurity:OrganizationMaintainerRole", + "IdentityOptions:Lockout:DefaultLockoutTimeSpan", + "IdentityOptions:Password:RepeatedResetPasswordTimeLimit", + "IdentityOptions:Password:RequireDigit", + "IdentityOptions:Password:RequiredLength", + "IdentityOptions:Password:RequireNonAlphanumeric", + "IdentityOptions:User:MaxPasswordAge", + "IdentityOptions:User:RemindPasswordExpiryInDay", + "IdentityOptions:User:RequireUniqueEmail", + "LoginPageUI:BackgroundUrl", + "LoginPageUI:PatternUrl", + "LoginPageUI:Preset", + "LoginPageUI:Presets:0:BackgroundUrl", + "LoginPageUI:Presets:0:Name", + "LoginPageUI:Presets:0:PatternUrl", + "LoginPageUI:Presets:1:BackgroundUrl", + "LoginPageUI:Presets:1:Name", + "LoginPageUI:Presets:1:PatternUrl", + "Notifications:DefaultSender", + "Notifications:Gateway", + "Notifications:SendGrid:ApiKey", + "Notifications:Smtp:ForceSslTls", + "Notifications:Smtp:Login", + "Notifications:Smtp:Port", + "Notifications:Smtp:SmtpServer", + "PushNotifications:ForceWebSockets", + "PushNotifications:HubUrl", + "PushNotifications:RedisBackplane:ChannelName", + "PushNotifications:ScalabilityMode", + "Search:AzureSearch:Key", + "Search:AzureSearch:SearchServiceName", + "Search:ContentFullTextSearchEnabled", + "Search:ElasticSearch:EnableHttpCompression", + "Search:ElasticSearch:Key", + "Search:ElasticSearch:Server", + "Search:ElasticSearch:User", + "Search:Lucene:Path", + "Search:OrderFullTextSearchEnabled", + "Search:PickupLocationFullTextSearchEnabled", + "Search:Provider", + "Search:Scope", + "SecurityHeaders", + "SecurityHeaders:FrameAncestors", + "SecurityHeaders:FrameOptions", + "Serilog:Enrich:0", + "Serilog:MinimumLevel:Default", + "Serilog:MinimumLevel:Override:Microsoft", + "Serilog:MinimumLevel:Override:Microsoft.Hosting.Lifetime", + "Serilog:MinimumLevel:Override:System", + "Serilog:MinimumLevel:Override:VirtoCommerce.Platform.Modules", + "Serilog:MinimumLevel:Override:VirtoCommerce.Platform.Web.Startup", + "Serilog:Using:0", + "Serilog:Using:1", + "Serilog:WriteTo:0", + "Serilog:WriteTo:1", + "VirtoCommerce:AllowInsecureHttp", + "VirtoCommerce:ApplicationInsights:EnableSqlCommandTextInstrumentation", + "VirtoCommerce:ApplicationInsights:IgnoreSqlTelemetryOptions:QueryIgnoreSubstrings:0", + "VirtoCommerce:ApplicationInsights:IgnoreSqlTelemetryOptions:QueryIgnoreSubstrings:1", + "VirtoCommerce:ApplicationInsights:IgnoreSqlTelemetryOptions:QueryIgnoreSubstrings:2", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Adaptive:EvaluationInterval", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Adaptive:InitialSamplingPercentage", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Adaptive:MaxSamplingPercentage", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Adaptive:MaxTelemetryItemsPerSecond", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Adaptive:MinSamplingPercentage", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Adaptive:MovingAverageRatio", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Adaptive:SamplingPercentageDecreaseTimeout", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Adaptive:SamplingPercentageIncreaseTimeout", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Fixed:SamplingPercentage", + "VirtoCommerce:ApplicationInsights:SamplingOptions:IncludedTypes", + "VirtoCommerce:ApplicationInsights:SamplingOptions:Processor", + "VirtoCommerce:DiscoveryPath", + "VirtoCommerce:GraphQL:ForbiddenAuthenticationTypes:0", + "VirtoCommerce:GraphQLPlayground:Enable", + "VirtoCommerce:Hangfire:AutomaticRetryCount", + "VirtoCommerce:Hangfire:JobStorageType", + "VirtoCommerce:Hangfire:MySqlStorageOptions:InvisibilityTimeout", + "VirtoCommerce:Hangfire:MySqlStorageOptions:QueuePollInterval", + "VirtoCommerce:Hangfire:PostgreSqlStorageOptions:DisableGlobalLocks", + "VirtoCommerce:Hangfire:PostgreSqlStorageOptions:InvisibilityTimeout", + "VirtoCommerce:Hangfire:PostgreSqlStorageOptions:QueuePollInterval", + "VirtoCommerce:Hangfire:PostgreSqlStorageOptions:UsePageLocksOnDequeue", + "VirtoCommerce:Hangfire:PostgreSqlStorageOptions:UseRecommendedIsolationLevel", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:CommandBatchMaxTimeout", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:DisableGlobalLocks", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:EnableHeavyMigrations", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:InactiveStateExpirationTimeout", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:QueuePollInterval", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:SlidingInvisibilityTimeout", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:TryAutoDetectSchemaDependentOptions", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:UseIgnoreDupKeyOption", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:UsePageLocksOnDequeue", + "VirtoCommerce:Hangfire:SqlServerStorageOptions:UseRecommendedIsolationLevel", + "VirtoCommerce:Hangfire:UseHangfireServer", + "VirtoCommerce:LicenseActivationUrl", + "VirtoCommerce:PlatformUI:Enable", + "VirtoCommerce:SampleDataUrl", + "VirtoCommerce:Stores:DefaultStore", + "VirtoCommerce:Swagger:Enable", + "VirtoCommerce:UseResponseCompression" + }; + + return MergeWithConfiguration(defaults, + options.Value?.WhiteList?.SettingKeys?.Include ?? [], + options.Value?.WhiteList?.SettingKeys?.Exclude ?? []); + } + } + + public Task> GetComparableSettingsAsync() + { + var result = AbstractTypeFactory.TryCreateInstance(); + result.ScopeName = "AppSettings"; + + foreach (var section in configuration.GetChildren().Where(x => VisibleSectionKeys.Contains(x.Key, StringComparer.OrdinalIgnoreCase))) + { + var sectionValues = new Dictionary(); + EnumerateSectionRecursive(section, section.Key, sectionValues); + + if (sectionValues.Count == 0) + { + continue; + } + + var resultGroup = AbstractTypeFactory.TryCreateInstance(); + resultGroup.GroupName = section.Key; + result.SettingGroups.Add(resultGroup); + + foreach (var key in sectionValues.Select(x => x.Key)) + { + var resultSetting = AbstractTypeFactory.TryCreateInstance(); + resultSetting.Name = key; + resultSetting.Value = sectionValues[key]; + resultSetting.IsSecret = !PublicSettingKeys.Contains(key, StringComparer.OrdinalIgnoreCase); + resultGroup.Settings.Add(resultSetting); + } + } + + return Task.FromResult((IList)[result]); + } + + private static void EnumerateSectionRecursive(IConfigurationSection section, string parentPath, Dictionary allValues) + { + foreach (var child in section.GetChildren()) + { + var currentPath = parentPath.IsNullOrEmpty() ? child.Key : $"{parentPath}:{child.Key}"; + + if (child.Value != null) + { + allValues[currentPath] = child.Value; + } + else + { + EnumerateSectionRecursive(child, currentPath, allValues); + } + } + } + + private IList MergeWithConfiguration(IList defaults, IEnumerable include, IEnumerable exclude) + { + // Start from defaults + var result = new List(defaults); + + // Apply includes (deduplicated, case-insensitive) + if (!include.IsNullOrEmpty()) + { + foreach (var key in include.Where(x => !string.IsNullOrWhiteSpace(x))) + { + if (!result.Contains(key, StringComparer.OrdinalIgnoreCase)) + { + result.Add(key); + } + } + } + + // Apply excludes (case-insensitive) + if (!exclude.IsNullOrEmpty()) + { + result = result + .Where(k => !exclude.Contains(k, StringComparer.OrdinalIgnoreCase)) + .ToList(); + } + + return result; + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableEnvironmentVariablesProvider.cs b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableEnvironmentVariablesProvider.cs new file mode 100644 index 0000000..4cb6559 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableEnvironmentVariablesProvider.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Newtonsoft.Json; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.EnvironmentsCompare.Data.Services; + +public class ComparableEnvironmentVariablesProvider(IWebHostEnvironment webHostEnvironment, IServer server) : IComparableSettingsProvider +{ + protected virtual IList VisibleVariables + { + get + { + return [ + "COMPUTERNAME", + "HOME", + "HOMEDRIVE", + "HOMEPATH", + "NUMBER_OF_PROCESSORS", + "OS", + "PATH", + "PROCESSOR_ARCHITECTURE", + "PROCESSOR_IDENTIFIER", + "PROCESSOR_LEVEL", + "PROCESSOR_REVISION", + "SESSIONNAME", + "SystemDrive", + "SystemRoot", + "TEMP", + "TMP", + "USERDOMAIN", + "USERNAME", + "USERPROFILE", + ]; + } + } + + public Task> GetComparableSettingsAsync() + { + var result = AbstractTypeFactory.TryCreateInstance(); + result.ScopeName = "Environment"; + + AddEnvironmentVariables(result); + + AddNetRuntime(result); + + AddServerFeatures(result); + + AddHosting(result); + + return Task.FromResult((IList)[result]); + } + + protected virtual void AddEnvironmentVariables(ComparableSettingScope result) + { + var variablesResultGroup = AbstractTypeFactory.TryCreateInstance(); + variablesResultGroup.GroupName = "Variables"; + result.SettingGroups.Add(variablesResultGroup); + + foreach (var keyValue in Environment.GetEnvironmentVariables()) + { + if (keyValue is not DictionaryEntry keyValueEntry || !VisibleVariables.Contains(keyValueEntry.Key?.ToString(), StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + var resultSetting = AbstractTypeFactory.TryCreateInstance(); + resultSetting.Name = keyValueEntry.Key.ToString(); + resultSetting.Value = keyValueEntry.Value?.ToString(); + variablesResultGroup.Settings.Add(resultSetting); + } + } + + protected virtual void AddNetRuntime(ComparableSettingScope result) + { + var netResultGroup = AbstractTypeFactory.TryCreateInstance(); + netResultGroup.GroupName = ".NET runtime"; + result.SettingGroups.Add(netResultGroup); + + var frameworkDescriptionResultSetting = AbstractTypeFactory.TryCreateInstance(); + frameworkDescriptionResultSetting.Name = nameof(RuntimeInformation.FrameworkDescription); + frameworkDescriptionResultSetting.Value = RuntimeInformation.FrameworkDescription; + netResultGroup.Settings.Add(frameworkDescriptionResultSetting); + + var osDescriptionResultSetting = AbstractTypeFactory.TryCreateInstance(); + osDescriptionResultSetting.Name = nameof(RuntimeInformation.OSDescription); + osDescriptionResultSetting.Value = RuntimeInformation.OSDescription; + netResultGroup.Settings.Add(osDescriptionResultSetting); + + var processArchitectureResultSetting = AbstractTypeFactory.TryCreateInstance(); + processArchitectureResultSetting.Name = nameof(RuntimeInformation.ProcessArchitecture); + processArchitectureResultSetting.Value = RuntimeInformation.ProcessArchitecture; + netResultGroup.Settings.Add(processArchitectureResultSetting); + + var runtimeIdentifierResultSetting = AbstractTypeFactory.TryCreateInstance(); + runtimeIdentifierResultSetting.Name = nameof(RuntimeInformation.RuntimeIdentifier); + runtimeIdentifierResultSetting.Value = RuntimeInformation.RuntimeIdentifier; + netResultGroup.Settings.Add(runtimeIdentifierResultSetting); + } + + protected virtual void AddServerFeatures(ComparableSettingScope result) + { + var serverResultGroup = AbstractTypeFactory.TryCreateInstance(); + serverResultGroup.GroupName = "Server"; + result.SettingGroups.Add(serverResultGroup); + + foreach (var feature in server.Features) + { + var featureResultSetting = AbstractTypeFactory.TryCreateInstance(); + featureResultSetting.Name = feature.Key.Name; + featureResultSetting.Value = JsonConvert.SerializeObject(feature.Value); + serverResultGroup.Settings.Add(featureResultSetting); + } + } + + protected virtual void AddHosting(ComparableSettingScope result) + { + var hostingResultGroup = AbstractTypeFactory.TryCreateInstance(); + hostingResultGroup.GroupName = "Hosting"; + result.SettingGroups.Add(hostingResultGroup); + + var applicationNameResultSetting = AbstractTypeFactory.TryCreateInstance(); + applicationNameResultSetting.Name = nameof(webHostEnvironment.ApplicationName); + applicationNameResultSetting.Value = webHostEnvironment.ApplicationName; + hostingResultGroup.Settings.Add(applicationNameResultSetting); + + var contentRootPathResultSetting = AbstractTypeFactory.TryCreateInstance(); + contentRootPathResultSetting.Name = nameof(webHostEnvironment.ContentRootPath); + contentRootPathResultSetting.Value = webHostEnvironment.ContentRootPath; + hostingResultGroup.Settings.Add(contentRootPathResultSetting); + + var environmentNameResultSetting = AbstractTypeFactory.TryCreateInstance(); + environmentNameResultSetting.Name = nameof(webHostEnvironment.EnvironmentName); + environmentNameResultSetting.Value = webHostEnvironment.EnvironmentName; + hostingResultGroup.Settings.Add(environmentNameResultSetting); + + var webRootPathResultSetting = AbstractTypeFactory.TryCreateInstance(); + webRootPathResultSetting.Name = nameof(webHostEnvironment.WebRootPath); + webRootPathResultSetting.Value = webHostEnvironment.WebRootPath; + hostingResultGroup.Settings.Add(webRootPathResultSetting); + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableModulesProvider.cs b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableModulesProvider.cs new file mode 100644 index 0000000..2a85ad9 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableModulesProvider.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Modularity; + +namespace VirtoCommerce.EnvironmentsCompare.Data.Services; + +public class ComparableModulesProvider(IModuleCatalog moduleCatalog) : IComparableSettingsProvider +{ + public Task> GetComparableSettingsAsync() + { + var result = AbstractTypeFactory.TryCreateInstance(); + result.ScopeName = "VirtoCommerce"; + + var modulesResultGroup = AbstractTypeFactory.TryCreateInstance(); + modulesResultGroup.GroupName = "Installed modules"; + result.SettingGroups.Add(modulesResultGroup); + + foreach (var module in moduleCatalog.Modules.OfType().Where(x => x.IsInstalled)) + { + var moduleSetting = AbstractTypeFactory.TryCreateInstance(); + moduleSetting.Name = module.ModuleName; + moduleSetting.Value = $"{module.Version} (state: {module.State})"; + modulesResultGroup.Settings.Add(moduleSetting); + } + + var platformResultGroup = AbstractTypeFactory.TryCreateInstance(); + platformResultGroup.GroupName = "Platform"; + result.SettingGroups.Add(platformResultGroup); + + var platformVersionSetting = AbstractTypeFactory.TryCreateInstance(); + platformVersionSetting.Name = "Version"; + platformVersionSetting.Value = PlatformVersion.CurrentVersion?.ToString(); + platformResultGroup.Settings.Add(platformVersionSetting); + + return Task.FromResult((IList)[result]); + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparablePlatformSettingsProvider.cs b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparablePlatformSettingsProvider.cs new file mode 100644 index 0000000..d9a2f3d --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparablePlatformSettingsProvider.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Settings; + +namespace VirtoCommerce.EnvironmentsCompare.Data.Services; + +public class ComparablePlatformSettingsProvider(ISettingsManager settingsManager) : IComparableSettingsProvider +{ + public async Task> GetComparableSettingsAsync() + { + var result = AbstractTypeFactory.TryCreateInstance(); + result.ScopeName = "PlatformSettings"; + + foreach (var group in settingsManager.AllRegisteredSettings + .GroupBy(x => x.GroupName)) + { + var resultGroup = AbstractTypeFactory.TryCreateInstance(); + resultGroup.GroupName = group.Key.IsNullOrEmpty() ? "Without group" : group.Key; + result.SettingGroups.Add(resultGroup); + + foreach (var setting in group) + { + var resultSetting = AbstractTypeFactory.TryCreateInstance(); + resultSetting.Name = setting.Name; + resultSetting.Value = await settingsManager.GetValueAsync(setting); + resultSetting.IsSecret = IsSettingSecret(setting); + resultGroup.Settings.Add(resultSetting); + } + } + + return [result]; + } + + protected virtual bool IsSettingSecret(SettingDescriptor setting) + { + return setting.ValueType == SettingValueType.SecureString; + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableSettingsMasterProvider.cs b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableSettingsMasterProvider.cs new file mode 100644 index 0000000..f1a2609 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableSettingsMasterProvider.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.EnvironmentsCompare.Data.Services; + +public class ComparableSettingsMasterProvider(IEnumerable comparableSettingsProviders) : IComparableSettingsMasterProvider +{ + public async Task> GetAllComparableSettingsAsync() + { + var result = new List(); + + foreach (var provider in comparableSettingsProviders) + { + result.AddRange(await GetSettingsFromProviderAsync(provider)); + } + + HideSecretSettings(result.SelectMany(x => x.SettingGroups).SelectMany(x => x.Settings)); + + return result; + } + + protected static async Task> GetSettingsFromProviderAsync(IComparableSettingsProvider comparableSettingsProvider) + { + IList result; + + try + { + result = await comparableSettingsProvider.GetComparableSettingsAsync(); + + if (result == null) + { + throw new InvalidOperationException("The provider returned null setting scopes list"); + } + } + catch (Exception ex) + { + var errorResultItem = AbstractTypeFactory.TryCreateInstance(); + errorResultItem.ErrorMessage = ex.Message; + + result = [errorResultItem]; + } + + var providerName = comparableSettingsProvider.GetType().FullName; + foreach (var resultItem in result) + { + resultItem.ProviderName = providerName; + } + + return result; + } + + protected virtual void HideSecretSettings(IEnumerable settings) + { + foreach (var setting in settings.Where(x => x.IsSecret)) + { + setting.Value = $"HASH: {(setting.Value ?? "").GetSHA1Hash()}"; + } + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableStoreSettingsProvider.cs b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableStoreSettingsProvider.cs new file mode 100644 index 0000000..65b40dc --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/ComparableStoreSettingsProvider.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.StoreModule.Core.Model.Search; +using VirtoCommerce.StoreModule.Core.Services; + +namespace VirtoCommerce.EnvironmentsCompare.Data.Services; + +public class ComparableStoreSettingsProvider(IStoreSearchService storeSearchService, ILocalizableSettingService localizableSettingService) : IComparableSettingsProvider +{ + protected const int MaxComparableStoresCount = 100; + + public async Task> GetComparableSettingsAsync() + { + var result = new List(); + + var storeSearchCriteria = AbstractTypeFactory.TryCreateInstance(); + storeSearchCriteria.Take = MaxComparableStoresCount; + + foreach (var store in await storeSearchService.SearchAllNoCloneAsync(storeSearchCriteria)) + { + var resultScope = AbstractTypeFactory.TryCreateInstance(); + resultScope.ScopeName = $"StoreSettings: {store.Id}"; + result.Add(resultScope); + + await AddSettings(resultScope, store); + + await AddLanguagesAndCurrencies(resultScope, store); + } + + return result; + } + + protected virtual async Task AddSettings(ComparableSettingScope resultScope, Store store) + { + foreach (var storeSettingGroup in store.Settings.GroupBy(x => x.GroupName)) + { + var resultGroup = AbstractTypeFactory.TryCreateInstance(); + resultGroup.GroupName = storeSettingGroup.Key.IsNullOrEmpty() ? "Without group" : storeSettingGroup.Key; + resultScope.SettingGroups.Add(resultGroup); + + foreach (var storeSetting in storeSettingGroup) + { + var resultSetting = AbstractTypeFactory.TryCreateInstance(); + resultSetting.Name = storeSetting.Name; + resultSetting.Value = await GetSettingValue(storeSetting, store); + resultSetting.IsSecret = IsSettingSecret(storeSetting); + resultGroup.Settings.Add(resultSetting); + } + } + } + + protected virtual async Task GetSettingValue(ObjectSettingEntry setting, Store store) + { + if (!setting.IsLocalizable) + { + return setting.Value; + } + else + { + var localizedValuesBuilder = new StringBuilder(); + + foreach (var language in store.Languages.OrderBy(x => x)) + { + localizedValuesBuilder.Append('[').Append(language).Append(']').Append(':'); + + foreach (var settingValue in await localizableSettingService.GetValuesAsync(setting.Name, language)) + { + localizedValuesBuilder.Append(settingValue.Key).Append('=').Append(settingValue.Value).Append(';'); + } + + localizedValuesBuilder.Append(';'); + } + + return localizedValuesBuilder.ToString(); + } + } + + protected virtual bool IsSettingSecret(ObjectSettingEntry setting) + { + return setting.ValueType == SettingValueType.SecureString; + } + + protected virtual Task AddLanguagesAndCurrencies(ComparableSettingScope resultScope, Store store) + { + var languagesAndCurrenciesGroup = AbstractTypeFactory.TryCreateInstance(); + languagesAndCurrenciesGroup.GroupName = "Languages and Currencies"; + resultScope.SettingGroups.Add(languagesAndCurrenciesGroup); + + var languagesSetting = AbstractTypeFactory.TryCreateInstance(); + languagesSetting.Name = nameof(store.Languages); + languagesSetting.Value = string.Join(";", store.Languages.OrderBy(x => x)); + languagesAndCurrenciesGroup.Settings.Add(languagesSetting); + + var defaultLanguageSetting = AbstractTypeFactory.TryCreateInstance(); + defaultLanguageSetting.Name = nameof(store.DefaultLanguage); + defaultLanguageSetting.Value = store.DefaultLanguage; + languagesAndCurrenciesGroup.Settings.Add(defaultLanguageSetting); + + var currenciesSetting = AbstractTypeFactory.TryCreateInstance(); + currenciesSetting.Name = nameof(store.Currencies); + currenciesSetting.Value = string.Join(";", store.Currencies.OrderBy(x => x)); + languagesAndCurrenciesGroup.Settings.Add(currenciesSetting); + + var defaultCurrencySetting = AbstractTypeFactory.TryCreateInstance(); + defaultCurrencySetting.Name = nameof(store.DefaultCurrency); + defaultCurrencySetting.Value = store.DefaultCurrency; + languagesAndCurrenciesGroup.Settings.Add(defaultCurrencySetting); + + return Task.CompletedTask; + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/EnvironmentsCompareClient.cs b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/EnvironmentsCompareClient.cs new file mode 100644 index 0000000..6f9c833 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/EnvironmentsCompareClient.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using VirtoCommerce.EnvironmentsCompare.Core; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.EnvironmentsCompare.Data.Services; + +public class EnvironmentsCompareClient(IHttpClientFactory httpClientFactory) : IEnvironmentsCompareClient +{ + public async Task> GetSettingsAsync(IList comparableEnvironments) + { + var httpClient = httpClientFactory.CreateClient(); + + var environmentTasks = comparableEnvironments.Select(async environment => + { + var result = AbstractTypeFactory.TryCreateInstance(); + result.EnvironmentName = environment.Name; + + try + { + using var httpRequest = new HttpRequestMessage(HttpMethod.Get, $"{environment.Url?.TrimEnd('/')}/{ModuleConstants.EnvironmentsCompare.ApiEnvironmentsCompareRoute}"); + httpRequest.Headers.TryAddWithoutValidation(ModuleConstants.EnvironmentsCompare.ApiAuthorizationKeyHeaderName, environment.ApiKey); + + using var httpResponse = await httpClient.SendAsync(httpRequest); + httpResponse.EnsureSuccessStatusCode(); + + var responseString = await httpResponse.Content.ReadAsStringAsync(); + var environmentSettings = JsonConvert.DeserializeObject>(responseString); + + result.SettingScopes = environmentSettings; + } + catch (Exception ex) + { + result.ErrorMessage = ex.Message; + } + + return result; + }).ToArray(); + + return [.. await Task.WhenAll(environmentTasks)]; + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/Services/EnvironmentsCompareService.cs b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/EnvironmentsCompareService.cs new file mode 100644 index 0000000..9ba3001 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/Services/EnvironmentsCompareService.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using VirtoCommerce.EnvironmentsCompare.Core; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.EnvironmentsCompare.Data.Services; + +public class EnvironmentsCompareService( + IOptions options, + IComparableSettingsMasterProvider comparableSettingsMasterProvider, + IEnvironmentsCompareClient environmentsCompareClient) + : IEnvironmentsCompareService +{ + protected const decimal DecimalComparisonEpsilon = 0.00001m; + + public async Task CompareAsync(IList environmentNames, string baseEnvironmentName = null, bool showAll = false) + { + var comparableEnvironmentSettings = await GetComparableEnvironmentsAsync(environmentNames); + + if (baseEnvironmentName == null || !comparableEnvironmentSettings.Any(x => x.EnvironmentName == baseEnvironmentName)) + { + baseEnvironmentName = comparableEnvironmentSettings.FirstOrDefault()?.EnvironmentName; + } + + var result = CompareEnvironmentSettings(comparableEnvironmentSettings, baseEnvironmentName); + + if (!showAll) + { + RemoveSettingsWithEqualValues(result); + } + + return result; + } + + public virtual async Task> GetComparableEnvironmentsAsync(IList environmentNames) + { + var comparableEnvironments = options.Value.ComparableEnvironments.Where(x => environmentNames.Contains(x.Name)).ToList(); + + var result = await environmentsCompareClient.GetSettingsAsync(comparableEnvironments); + + if (environmentNames.Contains(GetCurrentEnvironmentName())) + { + var currentEnvironmentSettings = AbstractTypeFactory.TryCreateInstance(); + currentEnvironmentSettings.IsCurrent = true; + currentEnvironmentSettings.SettingScopes = await comparableSettingsMasterProvider.GetAllComparableSettingsAsync(); + currentEnvironmentSettings.EnvironmentName = GetCurrentEnvironmentName(); + result.Add(currentEnvironmentSettings); + } + + return result; + } + + private string GetCurrentEnvironmentName() + { + return options.Value?.CurrentEnvironmentName ?? + ModuleConstants.EnvironmentsCompare.CurrentEnvironmentName; + } + + protected virtual SettingsComparisonResult CompareEnvironmentSettings(IList comparableEnvironmentSettings, string baseEnvironmentName) + { + var result = AbstractTypeFactory.TryCreateInstance(); + + foreach (var environment in comparableEnvironmentSettings) + { + var resultEnvironment = AbstractTypeFactory.TryCreateInstance(); + resultEnvironment.EnvironmentName = environment.EnvironmentName; + resultEnvironment.IsCurrent = environment.EnvironmentName == GetCurrentEnvironmentName(); + resultEnvironment.IsComparisonBase = environment.EnvironmentName == baseEnvironmentName; + resultEnvironment.ErrorMessage = environment.ErrorMessage; + result.ComparedEnvironments.Add(resultEnvironment); + } + + result.ComparedEnvironments = [.. result.ComparedEnvironments + .OrderBy(x => x.IsComparisonBase ? 0 : 1) + .ThenBy(x => x.IsCurrent ? 0 : 1)]; + + foreach (var scopeName in comparableEnvironmentSettings + .SelectMany(x => x.SettingScopes) + .Select(x => x.ScopeName) + .Distinct() + .Order()) + { + var resultScope = AbstractTypeFactory.TryCreateInstance(); + resultScope.ScopeName = scopeName; + result.SettingScopes.Add(resultScope); + + foreach (var groupName in comparableEnvironmentSettings + .SelectMany(x => x.SettingScopes) + .Where(x => x.ScopeName == scopeName) + .SelectMany(x => x.SettingGroups) + .Select(x => x.GroupName) + .Distinct() + .Order()) + { + var resultGroup = AbstractTypeFactory.TryCreateInstance(); + resultGroup.GroupName = groupName; + resultScope.SettingGroups.Add(resultGroup); + + foreach (var settingName in comparableEnvironmentSettings + .SelectMany(x => x.SettingScopes) + .Where(x => x.ScopeName == scopeName) + .SelectMany(x => x.SettingGroups) + .Where(x => x.GroupName == groupName) + .SelectMany(x => x.Settings) + .Select(x => x.Name) + .Distinct() + .Order()) + { + var resultSetting = CompareEnvironmentSetting(result.ComparedEnvironments, comparableEnvironmentSettings, baseEnvironmentName, scopeName, groupName, settingName); + + resultGroup.Settings.Add(resultSetting); + } + } + } + + return result; + } + + protected virtual ComparedEnvironmentSetting CompareEnvironmentSetting( + IList comparedEnvironments, + IList comparableEnvironmentSettings, + string baseEnvironmentName, + string scopeName, + string groupName, + string settingName) + { + var result = AbstractTypeFactory.TryCreateInstance(); + result.Name = settingName; + + var resultSettingBaseValue = AbstractTypeFactory.TryCreateInstance(); + resultSettingBaseValue.EnvironmentName = baseEnvironmentName; + result.ComparedValues.Add(resultSettingBaseValue); + + var resultSettingBaseValueFindResult = FindSettingValue(comparableEnvironmentSettings, baseEnvironmentName, scopeName, groupName, settingName); + resultSettingBaseValue.Value = resultSettingBaseValueFindResult.Value; + resultSettingBaseValue.ErrorMessage = resultSettingBaseValueFindResult.ErrorMessage; + resultSettingBaseValue.EqualsBaseValue = resultSettingBaseValueFindResult.Found; + + foreach (var comparableEnvironment in comparedEnvironments.Where(x => !x.IsComparisonBase)) + { + var resultSettingComparableValue = AbstractTypeFactory.TryCreateInstance(); + resultSettingComparableValue.EnvironmentName = comparableEnvironment.EnvironmentName; + result.ComparedValues.Add(resultSettingComparableValue); + + if (comparableEnvironment.ErrorMessage.IsNullOrEmpty()) + { + var resultSettingComparableValueFindResult = FindSettingValue(comparableEnvironmentSettings, comparableEnvironment.EnvironmentName, scopeName, groupName, settingName); + resultSettingComparableValue.Value = resultSettingComparableValueFindResult.Value; + resultSettingComparableValue.ErrorMessage = resultSettingComparableValueFindResult.ErrorMessage; + resultSettingComparableValue.EqualsBaseValue = resultSettingBaseValueFindResult.Found && resultSettingComparableValueFindResult.Found && SettingValuesAreEqual(resultSettingBaseValue, resultSettingComparableValue); + } + } + + return result; + } + + protected static (object Value, bool Found, string ErrorMessage) FindSettingValue(IList comparableEnvironmentSettings, string environmentName, string scopeName, string groupName, string settingName) + { + var environmentSettings = comparableEnvironmentSettings.FirstOrDefault(x => x.EnvironmentName == environmentName); + if (environmentSettings == null) + { + return (null, false, null); + } + + var scope = environmentSettings.SettingScopes.FirstOrDefault(x => x.ScopeName == scopeName); + if (scope == null) + { + return (null, false, "Setting scope not found"); + } + else if (!scope.ErrorMessage.IsNullOrEmpty()) + { + return (null, true, $"Setting scope (provider) error: {environmentSettings.ErrorMessage}"); + } + + var group = scope.SettingGroups.FirstOrDefault(x => x.GroupName == groupName); + if (group == null) + { + + return (null, false, "Setting group not found"); + } + + var setting = group.Settings.FirstOrDefault(x => x.Name == settingName); + if (setting == null) + { + return (null, false, "Setting not found"); + } + + return (setting.Value, true, null); + } + + protected virtual bool SettingValuesAreEqual(ComparedEnvironmentSettingValue baseValue, ComparedEnvironmentSettingValue comparableValue) + { + if (!baseValue.ErrorMessage.IsNullOrEmpty() || !comparableValue.ErrorMessage.IsNullOrEmpty()) + { + return false; + } + + if (baseValue.Value == null && comparableValue.Value == null) + { + return true; + } + else if (baseValue.Value == null || comparableValue.Value == null) + { + return false; + } + + return IsFloatingPointNumber(baseValue.Value) && IsFloatingPointNumber(comparableValue.Value) + ? Math.Abs(Convert.ToDecimal(baseValue.Value, CultureInfo.InvariantCulture) - Convert.ToDecimal(comparableValue.Value, CultureInfo.InvariantCulture)) < DecimalComparisonEpsilon + : baseValue.Value.ToString() == comparableValue.Value.ToString(); + } + + protected static bool IsFloatingPointNumber(object value) + { + return value is float or double or decimal; + } + + protected virtual void RemoveSettingsWithEqualValues(SettingsComparisonResult comparisonResult) + { + foreach (var scope in comparisonResult.SettingScopes.ToList()) + { + foreach (var group in scope.SettingGroups.ToList()) + { + foreach (var setting in group.Settings.ToList()) + { + RemoveSettingsWithEqualValues(group, setting); + } + + if (!group.Settings.Any()) + { + scope.SettingGroups.Remove(group); + } + } + + if (!scope.SettingGroups.Any()) + { + comparisonResult.SettingScopes.Remove(scope); + } + } + } + + protected virtual void RemoveSettingsWithEqualValues(ComparedEnvironmentSettingGroup group, ComparedEnvironmentSetting setting) + { + var allValuesEqual = !setting.ComparedValues.Any(x => x.EqualsBaseValue == false); + if (allValuesEqual) + { + group.Settings.Remove(setting); + } + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Data/VirtoCommerce.EnvironmentsCompare.Data.csproj b/src/VirtoCommerce.EnvironmentsCompare.Data/VirtoCommerce.EnvironmentsCompare.Data.csproj index 3c613ed..195e1b0 100644 --- a/src/VirtoCommerce.EnvironmentsCompare.Data/VirtoCommerce.EnvironmentsCompare.Data.csproj +++ b/src/VirtoCommerce.EnvironmentsCompare.Data/VirtoCommerce.EnvironmentsCompare.Data.csproj @@ -9,6 +9,7 @@ + diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Content/environments-compare.css b/src/VirtoCommerce.EnvironmentsCompare.Web/Content/environments-compare.css new file mode 100644 index 0000000..4494cf9 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Content/environments-compare.css @@ -0,0 +1,272 @@ +vc-environments-compare-search { + -o-flex-grow: 1; + -webkit-flex-grow: 1; + flex-grow: 1; +} + + +.comparison-header { + padding: 0; + margin: 0 -20px ; + font-size: 14px; +} + +.comparison-container { + padding: 0; + margin: 0 -20px 0; + overflow: auto; + position: relative; +} + +.comparison-container::after { + content: ""; + position: sticky; + right: 0; + top: 0; + bottom: 0; + width: 16px; + pointer-events: none; + background: linear-gradient(to left, rgba(0, 0, 0, 0.05), transparent); +} + +.comparison-list { + border-collapse: collapse; + table-layout: fixed; + font-size: 13px; + line-height: 1.4; +} + +.comparison-list thead tr { + background-color: #f9f9f9; + font-weight: bold; +} + +.comparison-list thead th { + position: sticky; + top: 0; + z-index: 2; + background-color: #f9f9f9; +} + +.comparison-list td, +.comparison-list th { + padding: 8px 10px; + overflow: hidden; + vertical-align: top; + word-wrap: break-word; +} + +tr.comparison-environments-summary { + background-color: #f9f9f9; + border-top: solid 1px #eaedf3; + border-bottom: solid 1px #eaedf3; +} + +td.comparison-environments-summary-title { + font-weight: bold; + padding-left: 20px; + border-right: solid 1px #eaedf3; +} + +th.environment-name-clickable { + cursor: pointer; +} + +th.environment-name-clickable:hover { + background-color: #eef3ff; +} + +th.environment-name-clickable .env-header-inner { + display: inline-flex; + align-items: center; + gap: 6px; +} + +th.environment-name-clickable .env-name { + white-space: nowrap; +} + +th.environment-name-clickable .fas, +th.environment-name-clickable .fa { + margin-right: 2px; +} + +tr.comparison-settings-scope-header { + background-color: #f9f9f9; +} + +td.comparison-settings-scope-name { + font-weight: bold; + padding-left: 20px; + border-right: solid 1px #eaedf3; +} + +tr.comparison-settings-group-header { + background-color: #f9f9f9; +} + +td.comparison-settings-group-name { + background-color: #f9f9f9; + font-weight: bold; + padding-left: 40px; + border-right: solid 1px #eaedf3; +} + +td.comparison-settings-name { + padding-left: 40px; + border-right: solid 1px #eaedf3; + font-family: monospace; + color: #555555; +} + +.comparison-settings-name-inner { + display: flex; + align-items: center; + gap: 6px; +} + +.comparison-settings-description-toggle { + cursor: pointer; +} + +.comparison-settings-description { + margin-top: 4px; + font-size: 12px; + font-style: italic; + color: brown; + word-wrap: break-word; + word-break: break-word; +} + +tr.comparison-diff { + background-color: #e9f3ff; +} + +td.comparison-diff { + background-color: #d6e7ff; + border-left: 3px solid #3b82f6; + font-weight: 500; +} + +td.environment-error, th.environment-error { + background-color: #f2f2f2; + color: #a94442; + font-style: italic; +} + +td.comparison-environments-no-data { + text-align: center; +} + +td[class^="scope-marker-"] { + background-color: transparent; + border-left-width: 4px; + border-left-style: solid; +} + +td.scope-marker-0 { + border-left-color: #82ccfc; +} + +td.scope-marker-1 { + border-left-color: #ff71eb; +} + +td.scope-marker-2 { + border-left-color: #82fce2; +} + +td.scope-marker-3 { + border-left-color: #bc71ff; +} + +td.scope-marker-4 { + border-left-color: #d6ff71; +} + +td.scope-marker-5 { + border-left-color: #72ff71; +} + +td.scope-marker-6 { + border-left-color: #71acff; +} + +td.scope-marker-7 { + border-left-color: #f599d4; +} + +.comparison-legend { + padding: 6px 20px; + font-size: 12px; + color: #666666; + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.comparison-legend .legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.comparison-legend .legend-color { + width: 12px; + height: 12px; + border-radius: 2px; +} + +.comparison-legend .legend-diff { + background-color: #d6e7ff; +} + +.comparison-legend .legend-error { + background-color: #f2f2f2; + border: 1px solid #cccccc; +} + +.comparison-filters { +} + +.comparison-filters .form-control { + max-width: 320px; +} + +.comparison-no-data { + padding: 40px 20px; + min-height: 260px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.comparison-no-data-inner { + max-width: 480px; +} + +.comparison-no-data-icon { + font-size: 48px; + color: #28a745; + margin-bottom: 12px; +} + +.comparison-no-data-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 8px; +} + +.comparison-no-data-text { + font-size: 13px; + color: #666666; + margin-bottom: 16px; +} + +.sticky-col { + position: sticky; + left: 0; + z-index: 1; + background-color: #ffffff; +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Controllers/Api/EnvironmentsCompareController.cs b/src/VirtoCommerce.EnvironmentsCompare.Web/Controllers/Api/EnvironmentsCompareController.cs index 2791a74..f382d1d 100644 --- a/src/VirtoCommerce.EnvironmentsCompare.Web/Controllers/Api/EnvironmentsCompareController.cs +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Controllers/Api/EnvironmentsCompareController.cs @@ -1,23 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using VirtoCommerce.EnvironmentsCompare.Core; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.EnvironmentsCompare.Web.Models; +using VirtoCommerce.Platform.Core.Common; using Permissions = VirtoCommerce.EnvironmentsCompare.Core.ModuleConstants.Security.Permissions; namespace VirtoCommerce.EnvironmentsCompare.Web.Controllers.Api; [Authorize] [Route("api/environments-compare")] -public class EnvironmentsCompareController : Controller +public class EnvironmentsCompareController(IOptions options, IEnvironmentsCompareService settingsCompareService) : Controller { - // GET: api/environments-compare - /// - /// Get message - /// - /// Return "Hello world!" message [HttpGet] - [Route("")] + [Route("get-environments")] [Authorize(Permissions.Read)] - public ActionResult Get() + public ActionResult> GetEnvironments() { - return Ok(new { result = "Hello world!" }); + var result = new List + { + new() + { + Url = $"{Request.Scheme}://{Request.Host}{Request.PathBase}", + Name = GetCurrentEnvironmentName(), + IsCurrent = true + } + }; + + if (options.Value != null && !options.Value.ComparableEnvironments.IsNullOrEmpty()) + { + result.AddRange(options.Value.ComparableEnvironments.Select(x => new EnvironmentResponseItem() { Name = x.Name, Url = x.Url })); + } + + return Ok(result); + } + + [HttpPost] + [Route("compare-environments")] + [Authorize(Permissions.Read)] + public async Task> CompareEnvironments([FromBody] CompareEnvironmentsRequest request) + { + if (request?.EnvironmentNames == null || request.EnvironmentNames.Count < 2) + { + return BadRequest("At least 2 environments are required for comparison."); + } + + var result = await settingsCompareService.CompareAsync(request.EnvironmentNames, request.BaseEnvironmentName, request.ShowAll); + return Ok(result); + } + + [HttpGet] + [Route("get-environment-settings/{environmentName}")] + [Authorize(Permissions.Read)] + public async Task> GetEnvironmentSettings(string environmentName) + { + if (environmentName.IsNullOrEmpty()) + { + return BadRequest("Environment name is required."); + } + + var environments = await settingsCompareService.GetComparableEnvironmentsAsync([environmentName]); + var environmentSettings = environments.FirstOrDefault(x => x.EnvironmentName == environmentName); + + if (environmentSettings == null) + { + return NotFound(); + } + + return Ok(environmentSettings); + } + + [HttpGet] + [Route("export-settings/{environmentName}")] + [Authorize(Permissions.Read)] + public async Task ExportSettings(string environmentName) + { + if (environmentName.IsNullOrEmpty()) + { + return BadRequest("Environment name is required for export."); + } + + var environment = await settingsCompareService.GetComparableEnvironmentsAsync([environmentName]); + + var serializerOptions = new JsonSerializerOptions { WriteIndented = true }; + var resultJson = JsonSerializer.Serialize(environment, serializerOptions); + var resultBytes = System.Text.Encoding.UTF8.GetBytes(resultJson); + var resultFileName = $"{environmentName}-settings-{DateTime.UtcNow.ToString("yyyy-dd-M--HH-mm-ss")}.json"; + + return File(resultBytes, "application/octet-stream", resultFileName); + } + + private string GetCurrentEnvironmentName() + { + return options.Value?.CurrentEnvironmentName ?? + ModuleConstants.EnvironmentsCompare.CurrentEnvironmentName; } } diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Controllers/Api/EnvironmentsCompareExternalController.cs b/src/VirtoCommerce.EnvironmentsCompare.Web/Controllers/Api/EnvironmentsCompareExternalController.cs new file mode 100644 index 0000000..e7ee038 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Controllers/Api/EnvironmentsCompareExternalController.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using VirtoCommerce.EnvironmentsCompare.Core; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using static VirtoCommerce.EnvironmentsCompare.Core.ModuleConstants.Security; + +namespace VirtoCommerce.EnvironmentsCompare.Web.Controllers.Api; + + +[Route(ModuleConstants.EnvironmentsCompare.ApiEnvironmentsCompareRoute)] +[Authorize] +public class EnvironmentsCompareExternalController(IComparableSettingsMasterProvider comparableSettingsMasterProvider) : Controller +{ + [HttpGet] + [Route("")] + [Authorize(Permissions.Read)] + public async Task>> GetSettings() + { + var result = await comparableSettingsMasterProvider.GetAllComparableSettingsAsync(); + return Ok(result); + } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/de.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/de.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..2ce66d4 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/de.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Umgebungsvergleich", + "blades": { + "environments-list": { + "title": "Umgebungsliste", + "toolbar": { + "compare": "Vergleichen" + }, + "labels": { + "name": "Name", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Umgebungsvergleich", + "labels": { + "setting-name": "Einstellung / Umgebung", + "environment-summary": "Umgebungsübersicht", + "no-differences-title": "Alle Umgebungen sind identisch", + "no-differences-text": "Für die ausgewählten Umgebungen wurden keine Konfigurationsunterschiede gefunden." + }, + "toolbar": { + "show-all": "Alle anzeigen", + "show-diff": "Nur Unterschiede anzeigen" + } + } + } + }, + "permissions": { + "environments-compare:access": "Mit dem Umgebungsvergleichsmodul arbeiten" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Umgebungsvergleich aktiviert", + "description": "Einstellung für aktivierten Umgebungsvergleich" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/en.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/en.VirtoCommerce.EnvironmentsCompare.json index 8ba1b09..abd524b 100644 --- a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/en.VirtoCommerce.EnvironmentsCompare.json +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/en.VirtoCommerce.EnvironmentsCompare.json @@ -1,23 +1,40 @@ { - "EnvironmentsCompare": { + "environments-compare": { + "main-menu-title": "Environments comparison", "blades": { - "hello-world": { - "title": "Hello world blade" + "environments-list": { + "title": "Environments list", + "toolbar": { + "compare": "Compare" + }, + "labels": { + "name": "Name", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Environments comparison", + "labels": { + "setting-name": "Setting / Environment", + "environment-summary": "Environment summary", + "no-differences-title": "All environments are identical", + "no-differences-text": "No configuration differences were found for the selected environments." + }, + "toolbar": { + "show-all": "Show all", + "show-diff": "Show differences only" + } } } }, "permissions": { - "environments-compare:access": "Open EnvironmentsCompare menu", - "environments-compare:create": "Create EnvironmentsCompare related data", - "environments-compare:read": "View EnvironmentsCompare related data", - "environments-compare:update": "Update EnvironmentsCompare related data", - "environments-compare:delete": "Delete EnvironmentsCompare related data" + "environments-compare:access": "Work with environments comparison module" }, "settings": { - "EnvironmentsCompare": { + "environments-compare": { "Enabled": { - "title": "EnvironmentsCompare Enabled", - "description": "EnvironmentsCompare Enabled setting" + "title": "Environments comparison Enabled", + "description": "Environments comparison Enabled setting" } } } diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/es.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/es.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..be80627 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/es.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Comparación de entornos", + "blades": { + "environments-list": { + "title": "Lista de entornos", + "toolbar": { + "compare": "Comparar" + }, + "labels": { + "name": "Nombre", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Comparación de entornos", + "labels": { + "setting-name": "Configuración / Entorno", + "environment-summary": "Resumen del entorno", + "no-differences-title": "Todos los entornos son idénticos", + "no-differences-text": "No se encontraron diferencias de configuración para los entornos seleccionados." + }, + "toolbar": { + "show-all": "Mostrar todo", + "show-diff": "Mostrar solo las diferencias" + } + } + } + }, + "permissions": { + "environments-compare:access": "Trabajar con el módulo de comparación de entornos" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Comparación de entornos activada", + "description": "Ajuste de comparación de entornos activado" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/fi.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/fi.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..ad0323c --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/fi.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Ympäristöjen vertailu", + "blades": { + "environments-list": { + "title": "Ympäristöluettelo", + "toolbar": { + "compare": "Vertaa" + }, + "labels": { + "name": "Nimi", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Ympäristöjen vertailu", + "labels": { + "setting-name": "Asetus / Ympäristö", + "environment-summary": "Ympäristön yhteenveto", + "no-differences-title": "Kaikki ympäristöt ovat identtisiä", + "no-differences-text": "Valituille ympäristöille ei löytynyt konfigurointieroja." + }, + "toolbar": { + "show-all": "Näytä kaikki", + "show-diff": "Näytä vain erot" + } + } + } + }, + "permissions": { + "environments-compare:access": "Työskentele ympäristöjen vertailumoduulin kanssa" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Ympäristöjen vertailu käytössä", + "description": "Ympäristöjen vertailu käytössä -asetus" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/fr.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/fr.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..e31eb5e --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/fr.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Comparaison des environnements", + "blades": { + "environments-list": { + "title": "Liste des environnements", + "toolbar": { + "compare": "Comparer" + }, + "labels": { + "name": "Nom", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Comparaison des environnements", + "labels": { + "setting-name": "Paramètre / Environnement", + "environment-summary": "Résumé de l'environnement", + "no-differences-title": "Tous les environnements sont identiques", + "no-differences-text": "Aucune différence de configuration n’a été trouvée pour les environnements sélectionnés." + }, + "toolbar": { + "show-all": "Tout afficher", + "show-diff": "Afficher uniquement les différences" + } + } + } + }, + "permissions": { + "environments-compare:access": "Utiliser le module de comparaison des environnements" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Comparaison des environnements activée", + "description": "Paramètre Comparaison des environnements activée" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/it.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/it.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..f5301b7 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/it.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Confronto ambienti", + "blades": { + "environments-list": { + "title": "Elenco ambienti", + "toolbar": { + "compare": "Confronta" + }, + "labels": { + "name": "Nome", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Confronto ambienti", + "labels": { + "setting-name": "Impostazione / Ambiente", + "environment-summary": "Riepilogo ambiente", + "no-differences-title": "Tutti gli ambienti sono identici", + "no-differences-text": "Non sono state trovate differenze di configurazione per gli ambienti selezionati." + }, + "toolbar": { + "show-all": "Mostra tutto", + "show-diff": "Mostra solo differenze" + } + } + } + }, + "permissions": { + "environments-compare:access": "Utilizza il modulo di confronto ambienti" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Confronto ambienti abilitato", + "description": "Impostazione Confronto ambienti abilitato" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/ja.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/ja.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..01c3f65 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/ja.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "環境比較", + "blades": { + "environments-list": { + "title": "環境リスト", + "toolbar": { + "compare": "比較" + }, + "labels": { + "name": "名前", + "url": "URL" + } + }, + "environments-comparison": { + "title": "環境比較", + "labels": { + "setting-name": "設定 / 環境", + "environment-summary": "環境サマリー", + "no-differences-title": "すべての環境は同一です", + "no-differences-text": "選択された環境間で設定の違いは見つかりませんでした。" + }, + "toolbar": { + "show-all": "すべて表示", + "show-diff": "相違点のみ表示" + } + } + } + }, + "permissions": { + "environments-compare:access": "環境比較モジュールを操作する" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "環境比較を有効にする", + "description": "環境比較を有効にする設定" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/no.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/no.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..d54d1c4 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/no.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Sammenligning av miljøer", + "blades": { + "environments-list": { + "title": "Miljøliste", + "toolbar": { + "compare": "Sammenlign" + }, + "labels": { + "name": "Navn", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Sammenligning av miljøer", + "labels": { + "setting-name": "Innstilling / Miljø", + "environment-summary": "Miljøsammendrag", + "no-differences-title": "Alle miljøene er identiske", + "no-differences-text": "Ingen konfigurasjonsforskjeller ble funnet for de valgte miljøene." + }, + "toolbar": { + "show-all": "Vis alle", + "show-diff": "Vis kun forskjeller" + } + } + } + }, + "permissions": { + "environments-compare:access": "Arbeid med modulen for sammenligning av miljøer" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Sammenligning av miljøer aktivert", + "description": "Innstilling for sammenligning av miljøer aktivert" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/pl.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/pl.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..7c64724 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/pl.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Porównanie środowisk", + "blades": { + "environments-list": { + "title": "Lista środowisk", + "toolbar": { + "compare": "Porównaj" + }, + "labels": { + "name": "Nazwa", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Porównanie środowisk", + "labels": { + "setting-name": "Ustawienie / Środowisko", + "environment-summary": "Podsumowanie środowiska", + "no-differences-title": "Wszystkie środowiska są identyczne", + "no-differences-text": "Nie znaleziono różnic w konfiguracji dla wybranych środowisk." + }, + "toolbar": { + "show-all": "Pokaż wszystko", + "show-diff": "Pokaż tylko różnice" + } + } + } + }, + "permissions": { + "environments-compare:access": "Praca z modułem porównywania środowisk" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Włączone porównywanie środowisk", + "description": "Ustawienie włączonego porównywania środowisk" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/pt.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/pt.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..34747ae --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/pt.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Comparação de ambientes", + "blades": { + "environments-list": { + "title": "Lista de ambientes", + "toolbar": { + "compare": "Comparar" + }, + "labels": { + "name": "Nome", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Comparação de ambientes", + "labels": { + "setting-name": "Definição / Ambiente", + "environment-summary": "Resumo do ambiente", + "no-differences-title": "Todos os ambientes são idênticos", + "no-differences-text": "Nenhuma diferença de configuração foi encontrada para os ambientes selecionados." + }, + "toolbar": { + "show-all": "Mostrar tudo", + "show-diff": "Mostrar apenas diferenças" + } + } + } + }, + "permissions": { + "environments-compare:access": "Trabalhar com o módulo de comparação de ambientes" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Comparação de ambientes ativada", + "description": "Definição de comparação de ambientes ativada" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/ru.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/ru.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..9840197 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/ru.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Сравнение сред", + "blades": { + "environments-list": { + "title": "Список сред", + "toolbar": { + "compare": "Сравнить" + }, + "labels": { + "name": "Имя", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Сравнение сред", + "labels": { + "setting-name": "Настройка / Среда", + "environment-summary": "Сводка по среде", + "no-differences-title": "Все среды идентичны", + "no-differences-text": "Для выбранных сред различий в конфигурации не обнаружено." + }, + "toolbar": { + "show-all": "Показать все", + "show-diff": "Показать только различия" + } + } + } + }, + "permissions": { + "environments-compare:access": "Работа с модулем сравнения сред" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Включено сравнение сред", + "description": "Параметр «Включено сравнение сред»" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/sv.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/sv.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..6988c63 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/sv.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "Miljöjämförelse", + "blades": { + "environments-list": { + "title": "Miljölista", + "toolbar": { + "compare": "Jämför" + }, + "labels": { + "name": "Namn", + "url": "URL" + } + }, + "environments-comparison": { + "title": "Miljöjämförelse", + "labels": { + "setting-name": "Inställning/Miljö", + "environment-summary": "Miljööversikt", + "no-differences-title": "Alla miljöer är identiska", + "no-differences-text": "Inga konfigurationsskillnader hittades för de valda miljöerna." + }, + "toolbar": { + "show-all": "Visa alla", + "show-diff": "Visa endast skillnader" + } + } + } + }, + "permissions": { + "environments-compare:access": "Arbeta med modulen för miljöjämförelse" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "Miljöjämförelse aktiverad", + "description": "Inställning för miljöjämförelse aktiverad" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/zh.VirtoCommerce.EnvironmentsCompare.json b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/zh.VirtoCommerce.EnvironmentsCompare.json new file mode 100644 index 0000000..d19c518 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Localizations/zh.VirtoCommerce.EnvironmentsCompare.json @@ -0,0 +1,41 @@ +{ + "environments-compare": { + "main-menu-title": "环境对比", + "blades": { + "environments-list": { + "title": "环境列表", + "toolbar": { + "compare": "对比" + }, + "labels": { + "name": "名称", + "url": "URL" + } + }, + "environments-comparison": { + "title": "环境对比", + "labels": { + "setting-name": "设置 / 环境", + "environment-summary": "环境摘要", + "no-differences-title": "所有环境都相同", + "no-differences-text": "在所选环境之间未发现配置差异。" + }, + "toolbar": { + "show-all": "显示全部", + "show-diff": "仅显示差异" + } + } + } + }, + "permissions": { + "environments-compare:access": "使用环境对比模块" + }, + "settings": { + "environments-compare": { + "Enabled": { + "title": "启用环境对比", + "description": "启用环境对比设置" + } + } + } +} \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Models/CompareEnvironmentsRequest.cs b/src/VirtoCommerce.EnvironmentsCompare.Web/Models/CompareEnvironmentsRequest.cs new file mode 100644 index 0000000..5e2190c --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Models/CompareEnvironmentsRequest.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.EnvironmentsCompare.Web.Models; + +public class CompareEnvironmentsRequest +{ + public IList EnvironmentNames { get; set; } + + public string BaseEnvironmentName { get; set; } + + public bool ShowAll { get; set; } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Models/EnvironmentResponseItem.cs b/src/VirtoCommerce.EnvironmentsCompare.Web/Models/EnvironmentResponseItem.cs new file mode 100644 index 0000000..6dc1d10 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Models/EnvironmentResponseItem.cs @@ -0,0 +1,10 @@ +namespace VirtoCommerce.EnvironmentsCompare.Web.Models; + +public class EnvironmentResponseItem +{ + public string Name { get; set; } + + public string Url { get; set; } + + public bool IsCurrent { get; set; } +} diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Module.cs b/src/VirtoCommerce.EnvironmentsCompare.Web/Module.cs index 9243350..35a7614 100644 --- a/src/VirtoCommerce.EnvironmentsCompare.Web/Module.cs +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Module.cs @@ -2,17 +2,20 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using VirtoCommerce.EnvironmentsCompare.Core; +using VirtoCommerce.EnvironmentsCompare.Core.Models; +using VirtoCommerce.EnvironmentsCompare.Core.Services; +using VirtoCommerce.EnvironmentsCompare.Data.MySql; +using VirtoCommerce.EnvironmentsCompare.Data.PostgreSql; +using VirtoCommerce.EnvironmentsCompare.Data.Repositories; +using VirtoCommerce.EnvironmentsCompare.Data.Services; +using VirtoCommerce.EnvironmentsCompare.Data.SqlServer; using VirtoCommerce.Platform.Core.Modularity; using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Core.Settings; using VirtoCommerce.Platform.Data.MySql.Extensions; using VirtoCommerce.Platform.Data.PostgreSql.Extensions; using VirtoCommerce.Platform.Data.SqlServer.Extensions; -using VirtoCommerce.EnvironmentsCompare.Core; -using VirtoCommerce.EnvironmentsCompare.Data.MySql; -using VirtoCommerce.EnvironmentsCompare.Data.PostgreSql; -using VirtoCommerce.EnvironmentsCompare.Data.Repositories; -using VirtoCommerce.EnvironmentsCompare.Data.SqlServer; namespace VirtoCommerce.EnvironmentsCompare.Web; @@ -23,6 +26,8 @@ public class Module : IModule, IHasConfiguration public void Initialize(IServiceCollection serviceCollection) { + serviceCollection.AddOptions().Bind(Configuration.GetSection("EnvironmentsCompare")); + serviceCollection.AddDbContext(options => { var databaseProvider = Configuration.GetValue("DatabaseProvider", "SqlServer"); @@ -42,12 +47,16 @@ public void Initialize(IServiceCollection serviceCollection) } }); - // Override models - //AbstractTypeFactory.OverrideType().MapToType(); - //AbstractTypeFactory.OverrideType(); + serviceCollection.AddTransient(); + + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); - // Register services - //serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } public void PostInitialize(IApplicationBuilder appBuilder) diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environment-item.html b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environment-item.html new file mode 100644 index 0000000..c4c0410 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environment-item.html @@ -0,0 +1,90 @@ +
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ 'environments-compare.blades.environments-comparison.labels.setting-name' | translate }} + + {{ blade.environmentName }} +
+ {{ 'environments-compare.blades.environments-comparison.labels.environment-summary' | translate }} + + {{ blade.envSettings.errorMessage }} +
{{ settingScope.scopeName }}
{{ settingGroup.groupName }}
+
+ {{ setting.name }} + + + +
+
+ {{ setting.descriptionText }} +
+
{{ setting.value }}
+
+
+
+
+ + diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environment-item.js b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environment-item.js new file mode 100644 index 0000000..0339758 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environment-item.js @@ -0,0 +1,106 @@ +angular.module('VirtoCommerce.EnvironmentsCompare') + .controller('VirtoCommerce.EnvironmentsCompare.environmentItemController', + [ + '$scope', + 'VirtoCommerce.EnvironmentsCompare.webApi', + '$translate', + function ( + $scope, + environmentsCompareApi, + $translate) { + const blade = $scope.blade; + + blade.title = blade.environmentName || 'environments-compare.blades.environments-comparison.title'; + blade.filter = blade.filter || {}; + if (typeof blade.hideEmptySections === 'undefined') { + blade.hideEmptySections = true; + } + + var filter = blade.filter; + filter.keyword = filter.keyword || ''; + filter.criteriaChanged = filter.criteriaChanged || function () { }; + + blade.settingScopes = []; + + function enrichSettingsDescriptions(settingScopes) { + if (!settingScopes) { + return; + } + + settingScopes.forEach(function (scope) { + if (!scope || !Array.isArray(scope.settingGroups)) { + return; + } + + scope.settingGroups.forEach(function (group) { + if (!group || !Array.isArray(group.settings)) { + return; + } + + group.settings.forEach(function (setting) { + if (!setting) { + return; + } + + var description = setting.description; + if (!description && setting.name) { + var key = 'settings.' + setting.name + '.description'; + var translated = $translate.instant(key); + if (translated && translated !== key) { + description = translated; + } + } + + if (description) { + setting.descriptionText = description; + } + }); + }); + }); + } + + blade.refresh = function () { + blade.isLoading = true; + + environmentsCompareApi.getEnvironmentSettings( + { environmentName: blade.environmentName }, + function (result) { + blade.envSettings = result; + blade.settingScopes = result && result.settingScopes ? result.settingScopes : []; + enrichSettingsDescriptions(blade.settingScopes); + blade.isLoading = false; + }, + function () { + blade.isLoading = false; + }); + }; + + blade.filterByText = function (setting) { + if (!blade.filter || !blade.filter.keyword) { + return true; + } + + const term = blade.filter.keyword.toLowerCase(); + return setting && setting.name && setting.name.toLowerCase().indexOf(term) !== -1; + } + + blade.groupHasVisibleSettings = function (settingGroup) { + if (!settingGroup || !Array.isArray(settingGroup.settings)) { + return false; + } + + return settingGroup.settings.some(blade.filterByText); + } + + blade.scopeHasVisibleSettings = function (settingScope) { + if (!settingScope || !Array.isArray(settingScope.settingGroups)) { + return false; + } + + return settingScope.settingGroups.some(blade.groupHasVisibleSettings); + } + + blade.refresh(); + }]); + + diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-comparison.html b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-comparison.html new file mode 100644 index 0000000..5e65efa --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-comparison.html @@ -0,0 +1,109 @@ +
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ 'environments-compare.blades.environments-comparison.labels.setting-name' | translate }} + + + + + {{ environment.environmentName }} + + +
{{ 'environments-compare.blades.environments-comparison.labels.environment-summary' | translate }}{{ environment.errorMessage }}
{{ settingScope.scopeName }}
{{ settingGroup.groupName }}
+
+ {{ setting.name }} + + + +
+
+ {{ setting.descriptionText }} +
+
{{ blade.getComparedValueOrError(setting, environment.environmentName) }}
+
+
+
+ +
+ {{ 'environments-compare.blades.environments-comparison.labels.no-differences-title' | translate }} +
+
+ {{ 'environments-compare.blades.environments-comparison.labels.no-differences-text' | translate }} +
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-comparison.js b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-comparison.js new file mode 100644 index 0000000..f4d9d44 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-comparison.js @@ -0,0 +1,249 @@ +angular.module('VirtoCommerce.EnvironmentsCompare') + .controller('VirtoCommerce.EnvironmentsCompare.environmentsComparisonController', + [ + '$scope', + 'VirtoCommerce.EnvironmentsCompare.webApi', + 'platformWebApp.uiGridHelper', + '$translate', + function ( + $scope, + environmentsCompareApi, + uiGridHelper, + $translate) { + const blade = $scope.blade; + + blade.title = 'environments-compare.blades.environments-comparison.title'; + blade.filter = blade.filter || {}; + if (typeof blade.hideEmptySections === 'undefined') { + blade.hideEmptySections = true; + } + + var filter = blade.filter; + filter.keyword = filter.keyword || ''; + filter.criteriaChanged = filter.criteriaChanged || function () { }; + + function recalculateWidths() { + const containerWidth = window.innerWidth || 1200; + blade.markerColWidth = 5; + blade.nameColWidth = Math.min(600, Math.max(300, containerWidth * 0.3)); + const envCount = blade.environmentNames && blade.environmentNames.length ? blade.environmentNames.length : 1; + const remaining = containerWidth - blade.markerColWidth - blade.nameColWidth; + blade.valueColWidth = Math.max(180, Math.floor(remaining / envCount)); + blade.totalWidth = blade.markerColWidth + blade.nameColWidth + blade.valueColWidth * envCount; + } + + recalculateWidths(); + + window.addEventListener('resize', recalculateWidths); + + function enrichSettingsDescriptions(settingScopes) { + if (!settingScopes) { + return; + } + + settingScopes.forEach(function (scope) { + if (!scope || !Array.isArray(scope.settingGroups)) { + return; + } + + scope.settingGroups.forEach(function (group) { + if (!group || !Array.isArray(group.settings)) { + return; + } + + group.settings.forEach(function (setting) { + if (!setting) { + return; + } + + var description = setting.description; + if (!description && setting.name) { + var key = 'settings.' + setting.name + '.description'; + var translated = $translate.instant(key); + if (translated && translated !== key) { + description = translated; + } + } + + if (description) { + setting.descriptionText = description; + } + }); + }); + }); + } + + blade.refresh = function () { + blade.isLoading = true; + + environmentsCompareApi.compareEnvironments( + { + environmentNames: blade.environmentNames, + baseEnvironmentName: blade.baseEnvironmentName, + showAll: blade.showAll + }, + function (compareEnvironmentsResult) { + blade.data = compareEnvironmentsResult; + if (blade.data && Array.isArray(blade.data.settingScopes)) { + enrichSettingsDescriptions(blade.data.settingScopes); + } + blade.isLoading = false; + }); + }; + + blade.setBaseEnvironment = function (environment) { + if (environment.isComparisonBase === true) { + return; + } + + blade.baseEnvironmentName = environment.environmentName; + blade.refresh(); + } + + blade.showAllSettings = function () { + blade.showAll = true; + blade.refresh(); + } + + blade.showDiffSettings = function () { + blade.showAll = false; + blade.refresh(); + } + + blade.getComparedValueOrError = function (settings, environmentName) { + if (!settings || !Array.isArray(settings.comparedValues)) { + return ''; + } + + const item = settings.comparedValues.find(x => x.environmentName === environmentName); + if (!item) { + return ''; + } + + return item.errorMessage || item.value; + } + + blade.environmentComparedValueHasDiff = function (settings, environmentName) { + if (!settings || !Array.isArray(settings.comparedValues)) { + return false; + } + + const comparedValue = settings.comparedValues.find(x => x.environmentName === environmentName); + return comparedValue ? blade.comparedValueHasDiff(comparedValue) : false; + } + + blade.environmentComparedValueHasError = function (settings, environmentName) { + if (!settings || !Array.isArray(settings.comparedValues)) { + return false; + } + + const comparedValue = settings.comparedValues.find(x => x.environmentName === environmentName); + return comparedValue ? blade.comparedValueHasError(comparedValue) : false; + } + + blade.anyEnvironmentComparedValueHasDiff = function (settings) { + if (!settings || !Array.isArray(settings.comparedValues)) { + return false; + } + + return settings.comparedValues.some(x => blade.comparedValueHasDiff(x)); + } + + blade.comparedValueHasDiff = function (item) { + if (!item) { + return false; + } + + return item.equalsBaseValue === false && !item.errorMessage; + } + + blade.comparedValueHasError = function (item) { + return !!(item && item.errorMessage); + } + + blade.filterByText = function (setting) { + if (!blade.filter || !blade.filter.keyword) { + return true; + } + + const term = blade.filter.keyword.toLowerCase(); + return setting && setting.name && setting.name.toLowerCase().indexOf(term) !== -1; + } + + blade.groupHasVisibleSettings = function (settingGroup) { + if (!settingGroup || !Array.isArray(settingGroup.settings)) { + return false; + } + + return settingGroup.settings.some(blade.filterByText); + } + + blade.scopeHasVisibleSettings = function (settingScope) { + if (!settingScope || !Array.isArray(settingScope.settingGroups)) { + return false; + } + + return settingScope.settingGroups.some(blade.groupHasVisibleSettings); + } + + blade.allEnvironmentsWithoutError = function () { + if (!blade.data || !Array.isArray(blade.data.comparedEnvironments)) { + return false; + } + + return blade.data.comparedEnvironments.every(function (env) { + return !env.errorMessage; + }); + } + + $scope.setGridOptions = function (gridOptions) { + uiGridHelper.initialize($scope, gridOptions, function (gridApi) { + $scope.gridApi = gridApi; + }); + }; + + function initializeToolbar() { + blade.toolbarCommands = [ + { + name: 'platform.commands.refresh', + icon: 'fa fa-refresh', + executeMethod: blade.refresh, + canExecuteMethod: function () { + return true; + } + }, + { + name: 'environments-compare.blades.environments-comparison.toolbar.show-all', + icon: 'fas fa-equals', + executeMethod: function () { + blade.showAllSettings(); + }, + hide: function () { + return blade.showAll === true; + }, + canExecuteMethod: function () { + return true; + } + }, + { + name: 'environments-compare.blades.environments-comparison.toolbar.show-diff', + icon: 'fas fa-not-equal', + executeMethod: function () { + blade.showDiffSettings(); + }, + hide: function () { + return !blade.showAll; + }, + canExecuteMethod: function () { + return true; + } + } + ]; + } + + blade.refresh(); + $scope.$on('$destroy', function () { + window.removeEventListener('resize', recalculateWidths); + }); + initializeToolbar(); + }]); \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-list.html b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-list.html new file mode 100644 index 0000000..35a806d --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-list.html @@ -0,0 +1,28 @@ +
+
+
+
+
+
+
{{ 'platform.list.no-data' | translate }}
+
+
+
+ + \ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-list.js b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-list.js new file mode 100644 index 0000000..73d5eeb --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/environments-list.js @@ -0,0 +1,108 @@ +angular.module('VirtoCommerce.EnvironmentsCompare') + .controller('VirtoCommerce.EnvironmentsCompare.environmentsListController', + [ + '$scope', + 'VirtoCommerce.EnvironmentsCompare.webApi', + 'platformWebApp.bladeNavigationService', 'platformWebApp.uiGridHelper', + function ( + $scope, + environmentsCompareApi, + bladeNavigationService, uiGridHelper) { + const blade = $scope.blade; + + blade.title = 'environments-compare.blades.environments-list.title'; + + blade.refresh = function () { + environmentsCompareApi.getEnvironments(function (getEnvironmentsResult) { + blade.data = getEnvironmentsResult; + blade.isLoading = false; + }); + }; + + blade.exportEnvironmentSettings = function () { + var environmentName = _.pluck($scope.gridApi.selection.getSelectedRows(), 'name')[0]; + + var a = document.createElement('a'); + a.href = 'api/environments-compare/export-settings/' + environmentName; + a.target = ' _blank'; + + document.body.appendChild(a); + a.click(); + + setTimeout(function () { + document.body.removeChild(a); + }, 100); + }; + + $scope.compare = function () { + const environmentNames = _.pluck($scope.gridApi.selection.getSelectedRows(), 'name'); + + const comparisonBlade = { + id: 'environments-comparison-blade', + controller: 'VirtoCommerce.EnvironmentsCompare.environmentsComparisonController', + template: 'Modules/$(VirtoCommerce.EnvironmentsCompare)/Scripts/blades/environments-comparison.html', + environmentNames: environmentNames, + baseEnvironmentName: environmentNames[0], + showAll: false, + }; + + bladeNavigationService.showBlade(comparisonBlade, blade); + }; + + $scope.setGridOptions = function (gridOptions) { + uiGridHelper.initialize($scope, gridOptions, function (gridApi) { + $scope.gridApi = gridApi; + }); + }; + + $scope.openEnvironment = function (environment) { + if (!environment || !environment.name) { + return; + } + + var newBlade = { + id: 'environment-item-blade-' + environment.name, + environmentName: environment.name, + controller: 'VirtoCommerce.EnvironmentsCompare.environmentItemController', + template: 'Modules/$(VirtoCommerce.EnvironmentsCompare)/Scripts/blades/environment-item.html' + }; + + bladeNavigationService.showBlade(newBlade, blade); + }; + + function initializeToolbar() { + blade.toolbarCommands = [ + { + name: 'platform.commands.refresh', + icon: 'fa fa-refresh', + executeMethod: blade.refresh, + canExecuteMethod: function () { + return true; + } + }, + { + name: 'environments-compare.blades.environments-list.toolbar.compare', + icon: 'fas fa-microscope', + executeMethod: $scope.compare, + canExecuteMethod: hasTwoOrMoreSelectedItems, + }, + { + name: 'platform.commands.export', + icon: 'fas fa-upload', + executeMethod: blade.exportEnvironmentSettings, + canExecuteMethod: hasOneSelectedItem, + }, + ]; + } + + function hasTwoOrMoreSelectedItems() { + return $scope.gridApi?.selection?.getSelectedRows()?.length >= 2; + } + + function hasOneSelectedItem() { + return $scope.gridApi?.selection?.getSelectedRows()?.length === 1; + } + + blade.refresh(); + initializeToolbar(); + }]); diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/hello-world.html b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/hello-world.html deleted file mode 100644 index a8d9856..0000000 --- a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/hello-world.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
-
-
{{blade.data}}
-
-
-
\ No newline at end of file diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/hello-world.js b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/hello-world.js deleted file mode 100644 index 04accf5..0000000 --- a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/blades/hello-world.js +++ /dev/null @@ -1,15 +0,0 @@ -angular.module('VirtoCommerce.EnvironmentsCompare') - .controller('VirtoCommerce.EnvironmentsCompare.helloWorldController', ['$scope', 'VirtoCommerce.EnvironmentsCompare.webApi', function ($scope, api) { - var blade = $scope.blade; - blade.title = 'EnvironmentsCompare'; - - blade.refresh = function () { - api.get(function (data) { - blade.title = 'EnvironmentsCompare.blades.hello-world.title'; - blade.data = data.result; - blade.isLoading = false; - }); - }; - - blade.refresh(); - }]); diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/directives/environmentsCompareSearch.js b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/directives/environmentsCompareSearch.js new file mode 100644 index 0000000..664e1c2 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/directives/environmentsCompareSearch.js @@ -0,0 +1,35 @@ +angular.module('VirtoCommerce.EnvironmentsCompare') + .directive('vcEnvironmentsCompareSearch', [function () { + return { + restrict: 'E', + templateUrl: function (elem, attrs) { + return attrs.templateUrl || + 'Modules/$(VirtoCommerce.EnvironmentsCompare)/Scripts/directives/environmentsCompareSearch.tpl.html'; + }, + scope: { + blade: '=' + }, + link: function ($scope) { + var blade = $scope.blade; + var filter = $scope.filter = blade.filter; + + if (!filter) { + filter = $scope.filter = blade.filter = { + keyword: '', + criteriaChanged: function () { } + }; + } else { + filter.keyword = filter.keyword || ''; + filter.criteriaChanged = filter.criteriaChanged || function () { }; + } + + filter.filterByKeyword = function () { + if (angular.isFunction(filter.criteriaChanged)) { + filter.criteriaChanged(); + } + }; + } + }; + }]); + + diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/directives/environmentsCompareSearch.tpl.html b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/directives/environmentsCompareSearch.tpl.html new file mode 100644 index 0000000..70d8c56 --- /dev/null +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/directives/environmentsCompareSearch.tpl.html @@ -0,0 +1,13 @@ +
+
+ + +
+
+ + diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/module.js b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/module.js index 2c24539..07ec6c1 100644 --- a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/module.js +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/module.js @@ -16,9 +16,9 @@ angular.module(moduleName, []) 'platformWebApp.bladeNavigationService', function (bladeNavigationService) { var newBlade = { - id: 'blade1', - controller: 'VirtoCommerce.EnvironmentsCompare.helloWorldController', - template: 'Modules/$(VirtoCommerce.EnvironmentsCompare)/Scripts/blades/hello-world.html', + id: 'environments-list-blade', + controller: 'VirtoCommerce.EnvironmentsCompare.environmentsListController', + template: 'Modules/$(VirtoCommerce.EnvironmentsCompare)/Scripts/blades/environments-list.html', isClosingDisabled: true, }; bladeNavigationService.showBlade(newBlade); @@ -32,8 +32,8 @@ angular.module(moduleName, []) //Register module in main menu var menuItem = { path: 'browse/environments-compare', - icon: 'fa fa-cube', - title: 'EnvironmentsCompare', + icon: 'fas fa-not-equal', + title: 'environments-compare.main-menu-title', priority: 100, action: function () { $state.go('workspace.EnvironmentsCompareState'); }, permission: 'environments-compare:access', diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/resources/environments-compare-api.js b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/resources/environments-compare-api.js index 029b831..a108e32 100644 --- a/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/resources/environments-compare-api.js +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/Scripts/resources/environments-compare-api.js @@ -1,4 +1,8 @@ angular.module('VirtoCommerce.EnvironmentsCompare') .factory('VirtoCommerce.EnvironmentsCompare.webApi', ['$resource', function ($resource) { - return $resource('api/environments-compare'); + return $resource('api/environments-compare', {}, { + getEnvironments: { method: 'GET', url: 'api/environments-compare/get-environments', isArray: true }, + compareEnvironments: { method: 'POST', url: 'api/environments-compare/compare-environments' }, + getEnvironmentSettings: { method: 'GET', url: 'api/environments-compare/get-environment-settings/:environmentName' }, + }); }]); diff --git a/src/VirtoCommerce.EnvironmentsCompare.Web/module.manifest b/src/VirtoCommerce.EnvironmentsCompare.Web/module.manifest index 894e709..87ab9b3 100644 --- a/src/VirtoCommerce.EnvironmentsCompare.Web/module.manifest +++ b/src/VirtoCommerce.EnvironmentsCompare.Web/module.manifest @@ -6,11 +6,11 @@ 3.876.0 - + - VirtoCommerce EnvironmentsCompare - VirtoCommerce EnvironmentsCompare + Environments Compare + Enables backend administrators to compare platform settings, environment configurations, and system information across multiple Virto Commerce environments Kirill Iusupov