Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSOE-638: Feature to edit shell settings from the admin in Lombiq.Hosting.Tenants #84

Merged
merged 64 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
7da1e96
Adding Shell Settings Editor display
wAsnk Sep 21, 2023
ce050bd
Trying to override shellsettings for tenant
wAsnk Sep 21, 2023
4803437
Return to editor
wAsnk Sep 21, 2023
8777f60
Cleaning up
wAsnk Sep 21, 2023
eb16def
Showing only related data
wAsnk Sep 26, 2023
4228b8f
Merge remote-tracking branch 'origin/dev' into issue/OSOE-638
wAsnk Sep 26, 2023
bdbee45
Merge remote-tracking branch 'origin/dev' into issue/OSOE-638
wAsnk Sep 26, 2023
ef4f375
Testing saving settings
wAsnk Sep 26, 2023
74a92bc
Merge remote-tracking branch 'origin/dev' into issue/OSOE-638
wAsnk Oct 2, 2023
0c809a8
Saving not already existing values.
wAsnk Oct 2, 2023
d7da847
Not needed anymore
wAsnk Oct 3, 2023
5ad6859
Cleanup and logic change
wAsnk Oct 3, 2023
2186685
Removing not needed usings
wAsnk Oct 3, 2023
7ac2d66
Merge remote-tracking branch 'origin/dev' into issue/OSOE-638
wAsnk Oct 3, 2023
971f1c2
Ignore spell check
wAsnk Oct 3, 2023
f97fa20
Removing lines
wAsnk Oct 3, 2023
4048834
Adding docs
wAsnk Oct 3, 2023
8ad8cd8
Fixing UI test
wAsnk Oct 4, 2023
a52bb2a
Using only what's necessary.
wAsnk Oct 4, 2023
d6ea51e
Update Lombiq.Hosting.Tenants.Management/Manifest.cs
wAsnk Oct 4, 2023
7535d01
Update Lombiq.Hosting.Tenants.Management/Readme.md
wAsnk Oct 4, 2023
47d797b
Update Lombiq.Hosting.Tenants.Management/Readme.md
wAsnk Oct 4, 2023
4b89d44
Make it possible to remove values
wAsnk Oct 4, 2023
2c056d3
Renaming
wAsnk Oct 4, 2023
9d6a1bb
Not needed
wAsnk Oct 4, 2023
ff90081
Code cleanup
wAsnk Oct 4, 2023
e782cd7
Not needed
wAsnk Oct 4, 2023
19c8660
Merge remote-tracking branch 'origin/issue/OSOE-638' into issue/OSOE-638
wAsnk Oct 4, 2023
024c4fc
Using distributed lock instead
wAsnk Oct 5, 2023
d004ff8
Cleanup
wAsnk Oct 5, 2023
5cfefa9
Adding hint
wAsnk Oct 5, 2023
dbd4bcb
Adding UI test
wAsnk Oct 5, 2023
c03aa91
Adding to solution
wAsnk Oct 5, 2023
8e1f939
Using alpha
wAsnk Oct 5, 2023
17f1d9a
Updating Tests UI version
wAsnk Oct 5, 2023
453b3e7
Setting nuget version back
wAsnk Oct 5, 2023
d96a5aa
Testing settings delete
wAsnk Oct 9, 2023
4196c68
Better key removal logic
wAsnk Oct 9, 2023
3d2bafa
Removing navigation to dashboard
wAsnk Oct 9, 2023
62aba3c
Suppressing warning
wAsnk Oct 9, 2023
b15fe7b
Modifying text
wAsnk Oct 9, 2023
427324a
Using already defined part
wAsnk Oct 9, 2023
0c905fb
Using Json nodes instead of key value pairs
wAsnk Oct 9, 2023
b0aff93
Adding docs
wAsnk Oct 9, 2023
ad27f5b
Updating UI test to match new logic
wAsnk Oct 9, 2023
cc5bd27
Update Lombiq.Hosting.Tenants.Management.Tests.UI/Extensions/TestCase…
wAsnk Oct 9, 2023
0f1ddcf
Update Lombiq.Hosting.Tenants.Management/Controllers/ShellSettingsEdi…
wAsnk Oct 9, 2023
c2820ea
Fresh copy and modifying again to accept simple string
wAsnk Oct 9, 2023
5c54b1d
Removing null check
wAsnk Oct 9, 2023
a01cd73
Using new constant
wAsnk Oct 9, 2023
6082053
Renaming field
wAsnk Oct 9, 2023
ba49da1
Using vars
wAsnk Oct 9, 2023
32de4ce
Renaming
wAsnk Oct 9, 2023
a8c1d5e
Adding notifier
wAsnk Oct 9, 2023
12922dc
Adding comment
wAsnk Oct 9, 2023
8c243f9
Using UpdateShellSettingsAsync to update version id and reload shell
wAsnk Oct 9, 2023
ea3a259
Adding test
wAsnk Oct 9, 2023
6394f2b
Adding comment
wAsnk Oct 11, 2023
e183f5a
Merge remote-tracking branch 'origin/dev' into issue/OSOE-638
wAsnk Oct 11, 2023
e130b24
Filling json from validation error
wAsnk Oct 12, 2023
b98f7fb
Adding test cases
wAsnk Oct 12, 2023
4edc564
Update Lombiq.Hosting.Tenants.Management/Service/JsonConfigurationPar…
wAsnk Oct 12, 2023
2800297
Update Lombiq.Hosting.Tenants.Management/Controllers/ShellSettingsEdi…
wAsnk Oct 12, 2023
83f9e95
Update UI Kit version
wAsnk Oct 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Lombiq.Tests.UI.Extensions;
using Lombiq.Tests.UI.Services;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Shouldly;
using System.Threading.Tasks;

namespace Lombiq.Hosting.Tenants.Management.Tests.UI.Extensions;

public static class TestCaseUITestContextExtensions
{
public static async Task TestShellSettingsEditorFeatureAsync(this UITestContext context)
{
await context.SignInDirectlyAsync();
await context.GoToAdminRelativeUrlAsync("/Tenants/Edit/Default");

// Expected JSON string.
#pragma warning disable JSON002 // Probable JSON string detected
await context.FillInEditorThenCheckValueAsync(
"{\"TestKey\":{\"TestSubKey\":{\"TestSubOptions\":{\"Test\": \"TestValue\"}}}}",
"TestValue");

await context.FillInEditorThenCheckValueAsync(
"{\"TestKey\":{\"TestSubKey\":{\"TestSubOptions\":{\"NewKey\": \"NewValue\"}}}}",
"NewValue");
Piedone marked this conversation as resolved.
Show resolved Hide resolved

await context.FillInEditorThenCheckValueAsync(
"{\"TestKey\":{\"TestSubKey\":{\"TestSubOptions\":{}}}}",
Piedone marked this conversation as resolved.
Show resolved Hide resolved
string.Empty);
#pragma warning restore JSON002 // Probable JSON string detected
await context.FillInEditorThenCheckValueAsync(
string.Empty,
string.Empty);
}

private static async Task FillInEditorThenCheckValueAsync(this UITestContext context, string text, string expectedValue)
{
context.FillInMonacoEditor("Json_editor", text);
await context.ClickReliablyOnAsync(By.XPath("//button[contains(.,'Save settings')]"));
var editorText = context.GetMonacoEditorText("Json_editor");

if (string.IsNullOrEmpty(expectedValue))
{
editorText.ShouldBeAsString(expectedValue);
}
else
{
var editorValue = JObject.Parse(editorText);
editorValue.SelectToken("TestKey.TestSubKey.TestSubOptions.Test")?.ToString().ShouldBeAsString(expectedValue);
}
}
}
13 changes: 13 additions & 0 deletions Lombiq.Hosting.Tenants.Management.Tests.UI/License.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright © 2021, [Lombiq Technologies Ltd.](https://lombiq.com)

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

- Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<PropertyGroup>
<Title>Lombiq Hosting - Tenants Management for Orchard Core - UI Test Extensions</Title>
<Authors>Lombiq Technologies</Authors>
<Copyright>Copyright © 2021, Lombiq Technologies Ltd.</Copyright>
<Description>Lombiq Hosting - Tenants Management for Orchard Core - UI Test Extensions: Extension methods that test tenants management for Orchard Core.</Description>
<PackageIcon>NuGetIcon.png</PackageIcon>
<PackageTags>OrchardCore;Lombiq;AspNetCore;Multitenancy;SaaS</PackageTags>
<RepositoryUrl>https://github.com/Lombiq/Hosting-Tenants</RepositoryUrl>
<PackageProjectUrl>https://github.com/Lombiq/Hosting-Tenants/blob/dev/Lombiq.Hosting.Tenants.Management.Tests.UI/Readme.md</PackageProjectUrl>
<PackageLicenseFile>License.md</PackageLicenseFile>
</PropertyGroup>

<ItemGroup Condition="'$(NuGetBuild)' != 'true'">
<ProjectReference Include="..\..\..\..\test\Lombiq.UITestingToolbox\Lombiq.Tests.UI\Lombiq.Tests.UI.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.Tests.UI" Version="8.0.2" />
</ItemGroup>

<ItemGroup>
<None Include="License.md" Pack="true" PackagePath="" />
<None Include="NuGetIcon.png" Pack="true" PackagePath="" />
<None Include="Readme.md" />
</ItemGroup>

</Project>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions Lombiq.Hosting.Tenants.Management.Tests.UI/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Lombiq Hosting - Tenants Management for Orchard Core - UI Test Extensions

## About

Extension methods that test tenants management for Orchard Core, with the help of [Lombiq UI Testing Toolbox for Orchard Core](https://github.com/Lombiq/UI-Testing-Toolbox).

Call these from a UI test project to verify the module's basic features; as seen in [Open-Source Orchard Core Extensions](https://github.com/Lombiq/Open-Source-Orchard-Core-Extensions).
Piedone marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public static class FeatureNames

public const string ForbiddenTenantNames = Module + "." + nameof(ForbiddenTenantNames);
public const string HideRecipesFromSetup = Module + "." + nameof(HideRecipesFromSetup);
public const string ShellSettingsEditor = Module + "." + nameof(ShellSettingsEditor);
}
Piedone marked this conversation as resolved.
Show resolved Hide resolved
Piedone marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using Lombiq.Hosting.Tenants.Management.Constants;
using Lombiq.Hosting.Tenants.Management.Models;
using Lombiq.Hosting.Tenants.Management.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Configuration;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.Locking.Distributed;
using OrchardCore.Modules;
using OrchardCore.Mvc.Core.Utilities;
using OrchardCore.Tenants.Controllers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using static OrchardCore.Tenants.Permissions;

namespace Lombiq.Hosting.Tenants.Management.Controllers;

[Feature(FeatureNames.ShellSettingsEditor)]
public class ShellSettingsEditorController : Controller
{
private readonly IAuthorizationService _authorizationService;
private readonly IShellHost _shellHost;
private readonly IShellConfigurationSources _shellConfigurationSources;
private readonly IDistributedLock _distributedLock;
private readonly INotifier _notifier;
private readonly IHtmlLocalizer<ShellSettingsEditorController> H;

public ShellSettingsEditorController(
IAuthorizationService authorizationService,
IShellHost shellHost,
IShellConfigurationSources shellConfigurationSources,
IDistributedLock distributedLock,
INotifier notifier,
IHtmlLocalizer<ShellSettingsEditorController> htmlLocalizer)
{
_authorizationService = authorizationService;
_shellHost = shellHost;
_shellConfigurationSources = shellConfigurationSources;
_distributedLock = distributedLock;
_notifier = notifier;
H = htmlLocalizer;
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(ShellSettingsEditorViewModel model)
{
if (!await _authorizationService.AuthorizeAsync(User, ManageTenants) ||
!_shellHost.TryGetSettings(model.TenantId, out var shellSettings))
{
return NotFound();
}

model.Json ??= "{}";
if (!IsValidJson(model.Json))
{
await _notifier.ErrorAsync(H["Please provide valid JSON input for shell settings."]);
return RedirectToAction(
nameof(AdminController.Edit),
typeof(AdminController).ControllerName(),
new
{
area = "OrchardCore.Tenants",
id = model.TenantId,
});
}
Piedone marked this conversation as resolved.
Show resolved Hide resolved

var tenantConfiguration = new JsonConfigurationParser().ParseConfiguration(model.Json);
var newTenantConfiguration = new Dictionary<string, string>();

var tenantSettingsPrefix = $"{model.TenantId}Prefix:";
var currentSettings = shellSettings.ShellConfiguration.AsEnumerable()
.Where(item => item.Value != null &&
item.Key.Contains(tenantSettingsPrefix))
.ToDictionary(key => key.Key.Replace(tenantSettingsPrefix, string.Empty), value => value.Value);

foreach (var key in tenantConfiguration.Keys)
{
var tenantSettingsPrefixWithKey = $"{tenantSettingsPrefix}{key}";
if (shellSettings[key] != tenantConfiguration[key])
{
newTenantConfiguration[tenantSettingsPrefixWithKey] = tenantConfiguration[key];
newTenantConfiguration[key] = tenantConfiguration[key];
}
}

var deletableKeys = currentSettings
.Where(item => !tenantConfiguration.ContainsKey(item.Key))
.Select(item => item.Key);

foreach (var key in deletableKeys)
{
var tenantSettingsPrefixWithKey = $"{tenantSettingsPrefix}{key}";
newTenantConfiguration[key] = null;
newTenantConfiguration[tenantSettingsPrefixWithKey] = null;
}

var (locker, locked) =
await _distributedLock.TryAcquireLockAsync(
"LOMBIQ_HOSTING_TENANTS_MANAGEMENT_SHELL_SETTINGS_EDITOR_LOCK",
TimeSpan.FromSeconds(10));

if (!locked)
{
throw new TimeoutException($"Failed to acquire a lock before saving settings to the tenant: {model.TenantId}.");
}

await using var acquiredLock = locker;

// We are using the shell configuration sources directly because using IShellHost.UpdateShellSettingsAsync would
// not save settings that has a key with multiple sections.
// See https://github.com/OrchardCMS/OrchardCore/issues/14481.
wAsnk marked this conversation as resolved.
Show resolved Hide resolved
await _shellConfigurationSources.SaveAsync(shellSettings.Name, newTenantConfiguration);
await _shellHost.UpdateShellSettingsAsync(shellSettings);

Piedone marked this conversation as resolved.
Show resolved Hide resolved
return RedirectToAction(
nameof(AdminController.Edit),
typeof(AdminController).ControllerName(),
new
{
area = "OrchardCore.Tenants",
id = model.TenantId,
});
}

private static bool IsValidJson(string json)
{
try
{
JsonDocument.Parse(json);
return true;
}
catch (JsonException)
{
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Lombiq.Hosting.Tenants.Management.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Layout;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.Mvc.Core.Utilities;
using OrchardCore.Tenants.Controllers;
using System.Threading.Tasks;

namespace Lombiq.Hosting.Tenants.Management.Filters;

public class ShellSettingsEditorFilter : IAsyncResultFilter
{
private readonly ILayoutAccessor _layoutAccessor;
private readonly IShapeFactory _shapeFactory;
private readonly IShellHost _shellHost;

public ShellSettingsEditorFilter(
ILayoutAccessor layoutAccessor,
IShapeFactory shapeFactory,
IShellHost shellHost)
{
_layoutAccessor = layoutAccessor;
_shapeFactory = shapeFactory;
_shellHost = shellHost;
}

public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
var actionRouteController = context.ActionDescriptor.RouteValues["Controller"];
var actionRouteArea = context.ActionDescriptor.RouteValues["Area"];
var actionRouteValue = context.ActionDescriptor.RouteValues["Action"];

if (actionRouteController == typeof(AdminController).ControllerName() &&
actionRouteArea == $"{nameof(OrchardCore)}.{nameof(OrchardCore.Tenants)}" &&
actionRouteValue is nameof(AdminController.Edit) &&
context.Result is ViewResult)
{
var tenantName = context.RouteData.Values["Id"].ToString();
if (!_shellHost.TryGetSettings(tenantName, out var shellSettings))
{
await next();
return;
}

var layout = await _layoutAccessor.GetLayoutAsync();
var contentZone = layout.Zones["Content"];
var tenantSettingsPrefix = $"{tenantName}Prefix";
var editableItems = shellSettings.ShellConfiguration.AsJsonNode();

await contentZone.AddAsync(
await _shapeFactory.CreateAsync<ShellSettingsEditorViewModel>(
"ShellSettingsEditor",
viewModel =>
{
viewModel.Json = editableItems[tenantSettingsPrefix]?.ToJsonString();
viewModel.TenantId = tenantName;
}),
"10");
}

await next();
}
}
9 changes: 9 additions & 0 deletions Lombiq.Hosting.Tenants.Management/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,12 @@
DefaultTenantOnly = true,
Dependencies = new[] { "OrchardCore.Setup" }
)]

[assembly: Feature(
Id = ShellSettingsEditor,
Name = "Lombiq Hosting - Tenants Management - Shell Settings Editor",
Description = "Adds a shell settings editor to the tenant editor page.",
Category = "Hosting",
DefaultTenantOnly = true,
Dependencies = new[] { "OrchardCore.Tenants" }
)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Lombiq.Hosting.Tenants.Management.Models;

public class ShellSettingsEditorViewModel
{
public string Json { get; set; }
public string TenantId { get; set; }
}
9 changes: 7 additions & 2 deletions Lombiq.Hosting.Tenants.Management/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

## About

With the help of this module, you can set restrictions on tenant creation.
With the help of this module, you can set restrictions on tenant creation and set tenant level shell settings in the tenant editor.

## Documentation

This module contains two features:
This module contains these features:

- `Lombiq.Hosting.Tenants.Management.ForbiddenTenantNames`
- `Lombiq.Hosting.Tenants.Management.HideRecipesFromSetup`
- `Lombiq.Hosting.Tenants.Management.ShellSettingsEditor`

### `Lombiq.Hosting.Tenants.Management.ForbiddenTenantNames`

Expand Down Expand Up @@ -42,3 +43,7 @@ public void ConfigureServices(IServiceCollection services) =>
```

**NOTE:** This extension method not only sets the tags you want to hide but also registers the feature as a setup feature. If you just want to use the default `HideFromSetupScreen` tag then just call the extension method without any parameter.

### `Lombiq.Hosting.Tenants.Management.ShellSettingsEditor`

Adds a shell settings editor to the tenant editor page where you can set values that are not already present in the current ShellSetting for the given tenant. Only those settings will be displayed that were added from this editor.
Loading