385 lines
12 KiB
Plaintext
385 lines
12 KiB
Plaintext
@using System.IO
|
|
@using System.Reflection
|
|
@using log4net
|
|
@using Newtonsoft.Json.Linq
|
|
@using Rms.Service.Bootstrap.Security
|
|
|
|
@inject IPasswordManager _passwordManager
|
|
|
|
@*
|
|
* dbMango
|
|
*
|
|
* Copyright 2025 Deutsche Bank AG
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*@
|
|
|
|
<style>
|
|
.textarea-animate {
|
|
-ms-transition: all 0.2s ease;
|
|
-o-transition: all 0.2s ease;
|
|
-webkit-transition: all 0.2s ease;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.button-panel {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end
|
|
}
|
|
|
|
.expand-icon {
|
|
margin-left: -30px;
|
|
z-index: 999;
|
|
margin-top: 6px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.file-input {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.file-input input[type=file] {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.column-name {
|
|
width: 25%;
|
|
}
|
|
|
|
.column-value {
|
|
width: 75%;
|
|
}
|
|
</style>
|
|
<div class="flex-stack-horizontal flex-align-end">
|
|
<button type="button" class="btn btn-secondary" @onclick="AddNewKeyValue" title="Add row">
|
|
<span class="ui-icon-font icon-plus-sm"></span>
|
|
</button>
|
|
@if (AllowImport)
|
|
{
|
|
<button type="button" class="btn btn-secondary mr-1 file-input" title="Import">
|
|
<InputFile OnChange="@OnInputFileChange" />
|
|
<span class="ui-icon-font icon-upload-sm"></span>
|
|
<span class="text-label pl-2">Import</span>
|
|
</button>
|
|
}
|
|
@if (ChildContent != null)
|
|
{
|
|
@ChildContent
|
|
}
|
|
</div>
|
|
<div class="pt-1 w-100">
|
|
<TableControl Class="table table-hover table-striped table-forge table-forge-numbered table-forge-top-align" Items="@Value.ToList()" PageSize="10" Filterable="false">
|
|
<TableColumnControl Name="Name" Field="Name" Class="column-name">
|
|
<Template Context="data">
|
|
@if (PossibleNames?.Count > 0)
|
|
{
|
|
<FormAutocompleteTextBox Value="@data.Row.Name" ValueChanged="@(x => SetName(data.Row, x))" Suggestions="@PossibleNames" />
|
|
}
|
|
else
|
|
{
|
|
<input class="input-group-text w-100"
|
|
value="@data.Row.Name"
|
|
@onchange="@(async e => await SetName(e, data.Row))"
|
|
style="text-align: left;"/>
|
|
}
|
|
</Template>
|
|
</TableColumnControl>
|
|
|
|
<TableColumnControl Name="Value" Field="Value" Class="column-value">
|
|
<Template Context="data">
|
|
<div class="d-flex">
|
|
<textarea class="w-100 form-control textarea-animate"
|
|
onfocus="this.rows = @CalculateSize(data.Row.Value);"
|
|
onfocusout="this.style.height = 'auto'; this.rows = 1"
|
|
oninput="this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px';"
|
|
onkeypress="return event.keyCode !== 13 || @Multiline.ToString().ToLower();"
|
|
value="@data.Row.Value"
|
|
@onchange="@(async e => await SetValue(e, data.Row))"
|
|
style="text-align: left;"
|
|
rows="1" cols="60"
|
|
>
|
|
</textarea>
|
|
@if (CalculateSize(data.Row.Value) > 1)
|
|
{
|
|
<i class="fas fa-angle-double-down expand-icon"></i>
|
|
}
|
|
</div>
|
|
</Template>
|
|
</TableColumnControl>
|
|
|
|
<TableColumnControl Name="" Field="Name">
|
|
<Template Context="data">
|
|
<div class="flex-stack-horizontal">
|
|
<button type="button" class="btn btn-secondary" @onclick="@(() => Delete(data.Row))" title="Delete row">
|
|
<span class="ui-icon-font icon-trash-sm"></span>
|
|
</button>
|
|
@{
|
|
var upDisabled = IsUpDisabled(data.Row);
|
|
var downDisabled = IsDownDisabled(data.Row);
|
|
}
|
|
<button type="button" class="btn btn-secondary" disabled="@upDisabled" @onclick="@(() => Up(data.Row))" title="Move row up">
|
|
<span class="ui-icon-font icon-arrow-up-sm"></span>
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" disabled="@downDisabled" @onclick="@(() => Down(data.Row))" title="Move row down">
|
|
<span class="ui-icon-font icon-arrow-down-sm"></span>
|
|
</button>
|
|
@if (EnableEncryption)
|
|
{
|
|
<button type="button" class="btn btn-secondary" @onclick="@(() => Encrypt(data.Row))" disabled="@(() => IsEncryptDisabled(data.Row))" title="Encrypt data file (irreversible!)">
|
|
<span class="ui-icon-font icon-lock-sm"></span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</Template>
|
|
</TableColumnControl>
|
|
</TableControl>
|
|
</div>
|
|
|
|
@code {
|
|
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!);
|
|
|
|
public class NameValuePair
|
|
{
|
|
public string Id { get; } = Guid.NewGuid().ToString("N");
|
|
|
|
public string Name { get; set; } = string.Empty;
|
|
public string Value { get; set; } = string.Empty;
|
|
}
|
|
|
|
[CascadingParameter] public IModalService Modal { get; set; } = null!;
|
|
|
|
[Parameter] public List<NameValuePair> Value { get; set; } = [];
|
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
|
|
|
[Parameter] public bool AllowImport { get; set; }
|
|
[Parameter] public bool Multiline { get; set; }
|
|
[Parameter] public bool EnableEncryption { get; set; } = true;
|
|
[Parameter] public bool EncryptAsFile { get; set; }
|
|
[Parameter] public bool ShowLabel { get; set; } = true;
|
|
[Parameter] public bool ImportSeparateValues { get; set; }
|
|
[Parameter] public List<string> PossibleNames { get; set; } = [];
|
|
[Parameter] public Func<string, string> GetDefaultValue { get; set; } = _ => string.Empty;
|
|
|
|
private static int CalculateSize(string value) => Math.Max(value.Split('\n').Length, value.Split('\r').Length);
|
|
|
|
#pragma warning disable 1998
|
|
private async Task SetValue(ChangeEventArgs e, NameValuePair data)
|
|
{
|
|
var val = e.Value?.ToString();
|
|
if (string.IsNullOrWhiteSpace(val))
|
|
return;
|
|
|
|
var dataRef = Value.First(x => x.Id == data.Id);
|
|
dataRef.Value = val;
|
|
data.Value = val;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private Task SetName(ChangeEventArgs e, NameValuePair data) => SetName(data, e.Value?.ToString());
|
|
|
|
private Task SetName(NameValuePair data, string? newName)
|
|
{
|
|
if ( string.IsNullOrWhiteSpace(newName))
|
|
return Task.CompletedTask;
|
|
|
|
var dataRef = Value.First(x => x.Id == data.Id);
|
|
dataRef.Name = newName;
|
|
data.Name = newName;
|
|
|
|
var v = GetDefaultValue(newName);
|
|
dataRef.Value = v;
|
|
data.Value = v;
|
|
|
|
StateHasChanged();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private void AddNewKeyValue()
|
|
{
|
|
Value.Add(new () { Name = string.Empty, Value = string.Empty });
|
|
StateHasChanged();
|
|
}
|
|
|
|
private Task Delete(NameValuePair data)
|
|
{
|
|
Value.Remove(data);
|
|
return InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
#pragma warning restore 1998
|
|
private async void OnInputFileChange(InputFileChangeEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
using var reader = new StreamReader(e.File.OpenReadStream());
|
|
var text = await reader.ReadToEndAsync();
|
|
|
|
if (ImportSeparateValues)
|
|
{
|
|
if (text.TrimStart().StartsWith("{")) // json
|
|
ImportJson(text);
|
|
else // try name=value
|
|
ImportText(text);
|
|
}
|
|
else
|
|
ImportAsWhole(e, text);
|
|
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Error(ex.Message, ex);
|
|
}
|
|
}
|
|
|
|
private void ImportAsWhole(InputFileChangeEventArgs e, string text)
|
|
{
|
|
Value.Add(new ()
|
|
{
|
|
Name = e.File.Name,
|
|
Value = text
|
|
});
|
|
}
|
|
|
|
private void ImportText(string text)
|
|
{
|
|
foreach (var s in text.Replace("\r","").Split("\n", StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var pos = s.IndexOf('=');
|
|
if (s.StartsWith('#') || pos > 0)
|
|
continue;
|
|
|
|
var name = s[..pos];
|
|
var val = s[(pos+1)..];
|
|
|
|
Value.Add(new ()
|
|
{
|
|
Name = name,
|
|
Value = val
|
|
});
|
|
}
|
|
}
|
|
|
|
private void ImportJson(string text)
|
|
{
|
|
var o = JObject.Parse(text);
|
|
foreach (var element in o.Properties())
|
|
{
|
|
var name = element.Name;
|
|
var val = element.Value.ToString();
|
|
|
|
Value.Add(new ()
|
|
{
|
|
Name = name,
|
|
Value = val
|
|
});
|
|
}
|
|
}
|
|
|
|
private bool IsUpDisabled(NameValuePair data)
|
|
{
|
|
var index = GetIndex(data);
|
|
return index <= 0;
|
|
}
|
|
|
|
private int GetIndex(NameValuePair data)
|
|
{
|
|
var index = Value.IndexOf(data);
|
|
if (index < 0)
|
|
{
|
|
var dataRef = Value.FirstOrDefault(x => x.Name == data.Name);
|
|
if ( dataRef == null)
|
|
return -1; // not found
|
|
index = Value.IndexOf(dataRef);
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
private Task Up(NameValuePair data)
|
|
{
|
|
var index = GetIndex(data);
|
|
if (index <= 0)
|
|
return Task.CompletedTask;
|
|
|
|
var dataRef = Value[index];
|
|
Value.RemoveAt(index);
|
|
Value.Insert(index - 1, dataRef);
|
|
return InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private bool IsDownDisabled(NameValuePair data)
|
|
{
|
|
var index = GetIndex(data);
|
|
return index >= Value.Count - 1;
|
|
}
|
|
|
|
private Task Down(NameValuePair data)
|
|
{
|
|
var index = GetIndex(data);
|
|
if (index >= Value.Count - 1)
|
|
return Task.CompletedTask;
|
|
|
|
var dataRef = Value[index];
|
|
Value.RemoveAt(index);
|
|
Value.Insert(index + 1, dataRef);
|
|
return InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private bool IsEncryptDisabled(NameValuePair data) => !EnableEncryption || (!EncryptAsFile && data.Value?.StartsWith('*') == true);
|
|
|
|
private async Task Encrypt(NameValuePair data)
|
|
{
|
|
var index = GetIndex(data);
|
|
if (index < 0 )
|
|
return;
|
|
|
|
var dataRef = Value[index];
|
|
|
|
var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, "Encrypt", $"Do you really want to encrypt entry \"{dataRef.Name}\"?<br>Note that this operation is <b>irreversible</b>!");
|
|
if ( res?.Cancelled ?? true )
|
|
return;
|
|
|
|
if (EncryptAsFile)
|
|
{
|
|
var enc = _passwordManager.EncryptFileContents(dataRef.Value);
|
|
var dec = _passwordManager.DecryptFileContents(enc);
|
|
|
|
if (dataRef.Value != dec)
|
|
throw new ApplicationException("Encryption is broken");
|
|
dataRef.Value = enc;
|
|
}
|
|
else
|
|
{
|
|
var enc = _passwordManager.EncryptPassword(dataRef.Value);
|
|
var dec = _passwordManager.DecryptPassword(enc);
|
|
|
|
if (dataRef.Value != dec)
|
|
throw new ApplicationException("Encryption is broken");
|
|
dataRef.Value = enc;
|
|
}
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
} |