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 12 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
Expand Up @@ -19,10 +19,18 @@ public static async Task TestShellSettingsEditorFeatureAsync(this UITestContext
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(
Piedone marked this conversation as resolved.
Show resolved Hide resolved
string.Empty,
expectedValue: null);
string.Empty);
}

private static async Task FillInEditorThenCheckValueAsync(this UITestContext context, string text, string expectedValue)
Expand All @@ -31,13 +39,13 @@ private static async Task FillInEditorThenCheckValueAsync(this UITestContext con
await context.ClickReliablyOnAsync(By.XPath("//button[contains(.,'Save settings')]"));
var editorText = context.GetMonacoEditorText("Json_editor");

if (string.IsNullOrEmpty(text))
if (string.IsNullOrEmpty(expectedValue))
{
editorText.ShouldBeAsString(text);
editorText.ShouldBeAsString(expectedValue);
}
else
{
var editorValue = JObject.Parse(context.GetMonacoEditorText("Json_editor"));
var editorValue = JObject.Parse(editorText);
editorValue.SelectToken("TestKey.TestSubKey.TestSubOptions.Test")?.ToString().ShouldBeAsString(expectedValue);
}
}
Expand Down
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
Expand Up @@ -3,7 +3,9 @@
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;
Expand All @@ -26,17 +28,23 @@ public class ShellSettingsEditorController : Controller
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)
IDistributedLock distributedLock,
INotifier notifier,
IHtmlLocalizer<ShellSettingsEditorController> htmlLocalizer)
{
_authorizationService = authorizationService;
_shellHost = shellHost;
_shellConfigurationSources = shellConfigurationSources;
_distributedLock = distributedLock;
_notifier = notifier;
H = htmlLocalizer;
}

[HttpPost]
Expand All @@ -52,6 +60,7 @@ public async Task<IActionResult> Edit(ShellSettingsEditorViewModel model)
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(),
Expand All @@ -62,49 +71,53 @@ public async Task<IActionResult> Edit(ShellSettingsEditorViewModel model)
});
}
Piedone marked this conversation as resolved.
Show resolved Hide resolved

var settingsDictionary = new JsonConfigurationParser().ParseConfiguration(model.Json);
var newSettings = new Dictionary<string, string>();
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);

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

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

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

var (locker, locked) = await _distributedLock.TryAcquireLockAsync("LOMBIQ_HOSTING_TENANTS_MANAGEMENT_SHELL_SETTINGS_EDITOR_LOCK", TimeSpan.FromSeconds(10));
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;

await _shellConfigurationSources.SaveAsync(shellSettings.Name, newSettings);
await _shellHost.ReloadShellContextAsync(shellSettings);
// 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),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
using Microsoft.Extensions.Configuration;
// This file is a copy and slight modification of Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser
// https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Configuration.Json/src/JsonConfigurationFileParser.cs.
// Their recommended way of using this class is to copy it. https://github.com/dotnet/runtime/issues/73946
wAsnk marked this conversation as resolved.
Show resolved Hide resolved
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;

namespace Lombiq.Hosting.Tenants.Management.Service;

public class JsonConfigurationParser
{
private readonly Dictionary<string, string> Data = new(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> Paths = new();
private readonly Dictionary<string, string> _configurationData = new(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> _paths = new();

public IDictionary<string, string> ParseConfiguration(string inputJson)
{
Expand All @@ -22,12 +24,13 @@ public IDictionary<string, string> ParseConfiguration(string inputJson)
using var doc = JsonDocument.Parse(inputJson, jsonDocumentOptions);
if (doc.RootElement.ValueKind != JsonValueKind.Object)
{
throw new FormatException("Format Exception:" + doc.RootElement.ValueKind);
throw new FormatException(
$"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.");
}

VisitObjectElement(doc.RootElement);

return Data;
return _configurationData;
}

private void VisitObjectElement(JsonElement element)
Expand All @@ -47,11 +50,11 @@ private void VisitObjectElement(JsonElement element)

private void VisitArrayElement(JsonElement element)
{
int index = 0;
var index = 0;

foreach (var arrayElement in element.EnumerateArray())
{
EnterContext(index.ToString(CultureInfo.InvariantCulture));
EnterContext(index.ToTechnicalString());
VisitValue(arrayElement);
ExitContext();
index++;
Expand All @@ -62,9 +65,9 @@ private void VisitArrayElement(JsonElement element)

private void SetNullIfElementIsEmpty(bool isEmpty)
{
if (isEmpty && Paths.Count > 0)
if (isEmpty && _paths.Count > 0)
{
Data[Paths.Peek()] = null;
_configurationData[_paths.Peek()] = null;
}
}

Expand All @@ -85,25 +88,25 @@ private void VisitValue(JsonElement value)
case JsonValueKind.True:
case JsonValueKind.False:
case JsonValueKind.Null:
string key = Paths.Peek();
if (Data.ContainsKey(key))
var key = _paths.Peek();
if (_configurationData.ContainsKey(key))
{
throw new FormatException("Key Is Duplicated:" + key);
throw new FormatException($"A duplicate key '{key}' was found.");
}

Data[key] = value.ToString();
_configurationData[key] = value.ToString();
break;

case JsonValueKind.Undefined:
default:
throw new FormatException("Unsupported JSON Token:" + value.ValueKind);
throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found.");
}
}

private void EnterContext(string context) =>
Paths.Push(Paths.Count > 0 ?
Paths.Peek() + ConfigurationPath.KeyDelimiter + context :
_paths.Push(_paths.Count > 0 ?
_paths.Peek() + ConfigurationPath.KeyDelimiter + context :
context);

private void ExitContext() => Paths.Pop();
private void ExitContext() => _paths.Pop();
}