add setting import

This commit is contained in:
sbwalker
2025-09-08 12:13:17 -04:00
parent dcc2e59e46
commit dfca6640da
9 changed files with 356 additions and 3 deletions

View File

@ -0,0 +1,56 @@
@namespace Oqtane.Modules.Admin.Settings
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ISettingService SettingService
@inject IStringLocalizer<ImportSettings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="settings" HelpText="Provide settings in comma delimited format using the column template specified" ResourceKey="Settings">Settings:</Label>
<div class="col-sm-9">
<textarea id="settings" class="form-control" @bind="@_settings" rows="5" required></textarea>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="Import">@Localizer["Import"]</button>&nbsp;
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _settings = "Entity,Id,Name,Value,Private\n";
public override string Title => "Import Settings";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
private async Task Import()
{
try
{
if (!string.IsNullOrEmpty(_settings))
{
ShowProgressIndicator();
var success = await SettingService.ImportSettingsAsync(_settings);
if (success)
{
AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error);
}
HideProgressIndicator();
}
else
{
AddModuleMessage(Localizer["Message.Import.Validation"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Importing Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Import"], MessageType.Error);
}
}
}

View File

@ -6,10 +6,11 @@
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-2">
<div class="col-sm-4">
<ActionLink Action="Add" Text="Add Setting" Security="SecurityAccessLevel.Host" ResourceKey="AddSetting" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_entityName, _entityId)))" />
<ActionLink Action="ImportSettings" Text="Import" Class="btn btn-secondary ms-1" Security="SecurityAccessLevel.Host" ResourceKey="ImportSettings" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_entityName, _entityId)))" />
</div>
<div class="col-sm-5">
<div class="col-sm-4">
<select class="form-select custom-select" value="@_entityName" @onchange="(e => EntityNameChanged(e))">
<option value="-">&lt;@Localizer["Select Entity"]&gt;</option>
@foreach (var entityName in _entityNames)
@ -18,7 +19,7 @@
}
</select>
</div>
<div class="col-sm-5">
<div class="col-sm-4">
<select class="form-select custom-select" value="@_entityId" @onchange="(e => EntityIdChanged(e))">
<option value="-">&lt;@Localizer["Select Id"]&gt;</option>
@foreach (var entityId in _entityIds)

View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Settings.Text" xml:space="preserve">
<value>Settings:</value>
</data>
<data name="Settings.HelpText" xml:space="preserve">
<value>Provide settings in comma delimited format using the column template specified</value>
</data>
<data name="Import" xml:space="preserve">
<value>Import</value>
</data>
<data name="Message.Import.Success" xml:space="preserve">
<value>Setting Import Successful</value>
</data>
<data name="Message.Import.Validation" xml:space="preserve">
<value>You Must Provide Settings To Import</value>
</data>
<data name="Message.Import.Failure" xml:space="preserve">
<value>Setting Import Failed. Please Review Your Event Log For More Detailed Information.</value>
</data>
<data name="Error.Import" xml:space="preserve">
<value>Error Importing Settings</value>
</data>
</root>

View File

@ -147,4 +147,7 @@
<data name="Value" xml:space="preserve">
<value>Value</value>
</data>
<data name="Import.SettingsText" xml:space="preserve">
<value>Import</value>
</data>
</root>

View File

@ -253,6 +253,13 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<int>> GetEntityIdsAsync(string entityName);
/// <summary>
/// Imports a list of settings
/// </summary>
/// <param name="settings"></param>
/// <returns></returns>
Task<bool> ImportSettingsAsync(string settings);
/// <summary>
/// Gets the value of the given settingName (key) from the given key-value dictionary
/// </summary>
@ -517,6 +524,11 @@ namespace Oqtane.Services
return await GetJsonAsync<List<int>>($"{Apiurl}/entityids?entityname={entityName}");
}
public async Task<bool> ImportSettingsAsync(string settings)
{
return await PostJsonAsync<bool>($"{Apiurl}/import?settings={settings}", true);
}
public string GetSetting(Dictionary<string, string> settings, string settingName, string defaultValue)
{
string value = defaultValue;

View File

@ -14,6 +14,8 @@ using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
using System.IO;
using System.Text.RegularExpressions;
namespace Oqtane.Controllers
{
@ -264,6 +266,70 @@ namespace Oqtane.Controllers
return _settings.GetEntityIds(entityName);
}
// POST api/<controller>/import?settings=x
[HttpPost("import")]
[Authorize(Roles = RoleNames.Host)]
public bool Import(string settings)
{
if (!string.IsNullOrEmpty(settings))
{
using (StringReader reader = new StringReader(settings))
{
// regex to split by comma - ignoring commas within double quotes
string pattern = ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)";
string row;
while ((row = reader.ReadLine()) != null)
{
List<string> cols = new List<string>();
string col = "";
int startIndex = 0;
MatchCollection matches = Regex.Matches(row, pattern);
foreach (Match match in matches)
{
col = row.Substring(startIndex, match.Index - startIndex);
if (col.StartsWith("\"") && col.EndsWith("\""))
{
col = col.Substring(1, col.Length - 2).Replace("\"\"", "\"");
}
cols.Add(col.Trim());
startIndex = match.Index + match.Length;
}
col = row.Substring(startIndex);
if (col.StartsWith("\"") && col.EndsWith("\""))
{
col = col.Substring(1, col.Length - 2).Replace("\"\"", "\"");
}
cols.Add(col.Trim());
if (cols.Count == 5 && cols[0].ToLower() != "entity" && int.TryParse(cols[1], out int entityId) && bool.TryParse(cols[4], out bool isPrivate))
{
var setting = _settings.GetSetting(cols[0], entityId, cols[2]);
if (setting == null)
{
_settings.AddSetting(new Setting { EntityName = cols[0], EntityId = entityId, SettingName = cols[2], SettingValue = cols[3], IsPrivate = isPrivate });
}
else
{
setting.SettingValue = cols[3];
setting.IsPrivate = isPrivate;
_settings.UpdateSetting(setting);
}
}
}
}
return true;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Settings Import Attempt {Settings}", settings);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return false;
}
}
// DELETE api/<controller>/clear
[HttpDelete("clear")]
[Authorize(Roles = RoleNames.Admin)]

View File

@ -833,6 +833,34 @@ namespace Oqtane.Infrastructure.SiteTemplates
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Setting Management",
Parent = "Admin",
Order = 67,
Path = "admin/settings",
Icon = Icons.Cog,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Settings.Index).ToModuleDefinitionName(), Title = "Setting Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
return pageTemplates;
}

View File

@ -87,6 +87,9 @@ namespace Oqtane.Infrastructure
case "6.1.5":
Upgrade_6_1_5(tenant, scope);
break;
case "6.2.0":
Upgrade_6_2_0(tenant, scope);
break;
}
}
}
@ -557,6 +560,43 @@ namespace Oqtane.Infrastructure
RemoveAssemblies(tenant, assemblies, "6.1.5");
}
private void Upgrade_6_2_0(Tenant tenant, IServiceScope scope)
{
var pageTemplates = new List<PageTemplate>
{
new PageTemplate
{
Update = false,
Name = "Setting Management",
Parent = "Admin",
Order = 67,
Path = "admin/settings",
Icon = Icons.Cog,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Settings.Index).ToModuleDefinitionName(), Title = "Setting Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
}
};
AddPagesToSites(scope, tenant, pageTemplates);
}
private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates)
{

View File

@ -1,9 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Modules.Admin.Users;
using Oqtane.Shared;
namespace Oqtane.Repository
@ -21,6 +26,7 @@ namespace Oqtane.Repository
void DeleteSettings(string entityName, int entityId);
IEnumerable<string> GetEntityNames();
IEnumerable<int> GetEntityIds(string entityName);
string GetSettingValue(IEnumerable<Setting> settings, string settingName, string defaultValue);
string GetSettingValue(string entityName, int entityId, string settingName, string defaultValue);
}