330 lines
14 KiB
Plaintext
330 lines
14 KiB
Plaintext
@implements IDisposable
|
|
|
|
<div class="d-flex w-100 h-100 flex-column">
|
|
<EditForm EditContext="EditContext">
|
|
<DataAnnotationsValidator />
|
|
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="serialNumber">Serial Number</label>
|
|
<InputText id="serialNumber" class="form-control" @bind-Value="Model.SerialNumber" />
|
|
<ValidationMessage For="@(() => Model.SerialNumber)" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="prefix">Topic Prefix</label>
|
|
<InputText id="prefix" class="form-control" @bind-Value="Model.VDA5050TopicPrefix"/>
|
|
<ValidationMessage For="@(() => Model.VDA5050TopicPrefix)" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="manufacturer">Manufacturer</label>
|
|
<InputText id="manufacturer" class="form-control" @bind-Value="Model.VDA5050Manufacturer" disabled="true" />
|
|
<ValidationMessage For="@(() => Model.VDA5050Manufacturer)" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="version">Version</label>
|
|
<InputText id="version" class="form-control" @bind-Value="Model.VDA5050Version" disabled="true" />
|
|
<ValidationMessage For="@(() => Model.VDA5050Version)" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="host">Host</label>
|
|
<InputText id="host" class="form-control" @bind-Value="Model.VDA5050HostServer" />
|
|
<ValidationMessage For="@(() => Model.VDA5050HostServer)" />
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label" for="port">Port</label>
|
|
<InputNumber id="port" class="form-control" @bind-Value="Model.VDA5050Port" />
|
|
<ValidationMessage For="@(() => Model.VDA5050Port)" />
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label" for="publishRepeat">Publish Repeat</label>
|
|
<InputNumber id="publishRepeat" class="form-control" @bind-Value="Model.VDA5050PublishRepeat" />
|
|
<ValidationMessage For="@(() => Model.VDA5050PublishRepeat)" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="username">Username</label>
|
|
<InputText id="username" class="form-control" @bind-Value="Model.VDA5050UserName" />
|
|
<ValidationMessage For="@(() => Model.VDA5050UserName)" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="password">Password</label>
|
|
<div class="password-input-wrapper">
|
|
<InputText id="password" class="form-control password-input" @bind-Value="Model.VDA5050Password" inputmode="text" type="@PasswordInputType" autocomplete="new-password" spellcheck="false" />
|
|
<button class="password-toggle-btn" type="button" @onclick="TogglePasswordVisibility" aria-label="Toggle password visibility">
|
|
<i class="@PasswordIconClass" aria-hidden="true"></i>
|
|
</button>
|
|
</div>
|
|
<ValidationMessage For="@(() => Model.VDA5050Password)" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-check mb-2">
|
|
<InputCheckbox id="enablePassword" class="form-check-input" @bind-Value="Model.VDA5050EnablePassword" />
|
|
<label class="form-check-label" for="enablePassword">Enable Password</label>
|
|
<ValidationMessage For="@(() => Model.VDA5050EnablePassword)" />
|
|
</div>
|
|
|
|
<div class="form-check mb-2">
|
|
<InputCheckbox id="enableTls" class="form-check-input" @bind-Value="Model.VDA5050EnableTls" />
|
|
<label class="form-check-label" for="enableTls">Enable TLS</label>
|
|
<ValidationMessage For="@(() => Model.VDA5050EnableTls)" />
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<label class="form-label" for="caFile">CA File</label>
|
|
<div class="custom-file-input-wrapper position-relative">
|
|
<InputFile class="d-none" id="caFile" OnChange="e => OnFileSelected(e, FileSlot.Ca)" accept=".crt,.pem,.cer,.pfx,.key,.jks" />
|
|
<div class="form-control d-flex align-items-center gap-2 ps-0">
|
|
<label for="caFile" class="upload-btn d-flex align-items-center gap-1">
|
|
<i class="mdi mdi-attachment"></i>
|
|
</label>
|
|
|
|
<span id="fileNameDisplay" class="text-muted flex-grow-1 text-truncate" style="max-width: 200px;">
|
|
@Model.VDA5050CA
|
|
</span>
|
|
|
|
<button class="password-toggle-btn position-absolute rounded-end-2 top-50 translate-middle-y" type="button" @onclick="() => RemoveFile(FileSlot.Ca)" aria-label="Remove client certificate file">
|
|
<i class="mdi mdi-close" aria-hidden="true"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@if (!string.IsNullOrEmpty(CaFileInfo))
|
|
{
|
|
<div class="small text-muted mt-1 mb-1">@CaFileInfo</div>
|
|
}
|
|
@if (!string.IsNullOrEmpty(CaFileError))
|
|
{
|
|
<div class="text-danger small mt-1 mb-1">@CaFileError</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<label class="form-label" for="clientCertFile">Client Certificate File</label>
|
|
<div class="custom-file-input-wrapper position-relative">
|
|
<InputFile class="d-none" id="clientCertFile" OnChange="e => OnFileSelected(e, FileSlot.Cert)" accept=".crt,.pem,.cer,.pfx,.key,.jks" />
|
|
<div class="form-control d-flex align-items-center gap-2 ps-0">
|
|
<label for="clientCertFile" class="upload-btn d-flex align-items-center gap-1">
|
|
<i class="mdi mdi-attachment"></i>
|
|
</label>
|
|
|
|
<span id="fileNameDisplay" class="text-muted flex-grow-1 text-truncate" style="max-width: 200px;">
|
|
@Model.VDA5050Cer
|
|
</span>
|
|
|
|
<button class="password-toggle-btn position-absolute rounded-end-2 top-50 translate-middle-y" type="button" @onclick="() => RemoveFile(FileSlot.Cert)" aria-label="Remove client certificate file">
|
|
<i class="mdi mdi-close" aria-hidden="true"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@if (!string.IsNullOrEmpty(CertFileInfo))
|
|
{
|
|
<div class="small text-muted mt-1 mb-1">@CertFileInfo</div>
|
|
}
|
|
@if (!string.IsNullOrEmpty(CertFileError))
|
|
{
|
|
<div class="text-danger small mt-1 mb-1">@CertFileError</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<label class="form-label" for="clientKeyFile">Client Key File</label>
|
|
<div class="custom-file-input-wrapper position-relative">
|
|
<InputFile class="d-none" id="clientKeyFile" OnChange="e => OnFileSelected(e, FileSlot.Key)" accept=".crt,.pem,.cer,.pfx,.key,.jks" />
|
|
<div class="form-control d-flex align-items-center gap-2 ps-0">
|
|
<label for="clientKeyFile" class="upload-btn d-flex align-items-center gap-1">
|
|
<i class="mdi mdi-attachment"></i>
|
|
</label>
|
|
|
|
<span id="fileNameDisplay" class="text-muted flex-grow-1 text-truncate" style="max-width: 200px;">
|
|
@Model.VDA5050Key
|
|
</span>
|
|
|
|
<button class="password-toggle-btn position-absolute rounded-end-2 top-50 translate-middle-y" type="button" @onclick="() => RemoveFile(FileSlot.Key)" aria-label="Remove client key file">
|
|
<i class="mdi mdi-close" aria-hidden="true"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@if (!string.IsNullOrEmpty(KeyFileInfo))
|
|
{
|
|
<div class="small text-muted mt-1 mb-1">@KeyFileInfo</div>
|
|
}
|
|
@if (!string.IsNullOrEmpty(KeyFileError))
|
|
{
|
|
<div class="text-danger small mt-1 mb-1">@KeyFileError</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<label class="form-label" for="description">Description</label>
|
|
<InputTextArea id="description m-1" class="form-control" @bind-Value="Model.Description" />
|
|
<ValidationMessage For="@(() => Model.Description)" />
|
|
</div>
|
|
</EditForm>
|
|
<div class="flex-grow-1" />
|
|
<div>
|
|
@if (Model.CreatedAt != default || Model.UpdatedAt != default)
|
|
{
|
|
<div class="d-flex justify-content-end mt-2">
|
|
<small class="text-muted">Created: @Model.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
|
|
<small class="text-muted ms-3">Updated: @Model.UpdatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter]
|
|
public RobotVDA5050ConfigDto Model { get; set; } = new();
|
|
|
|
[Parameter]
|
|
public EventCallback<RobotVDA5050ConfigDto> ModelChanged { get; set; }
|
|
|
|
public IBrowserFile? CaFile { get; set; }
|
|
public IBrowserFile? CertFile { get; set; }
|
|
public IBrowserFile? KeyFile { get; set; }
|
|
public long MaxFileSize { get; set; } = 10 * 1024 * 1024;
|
|
|
|
private EditContext? EditContext;
|
|
private bool showPassword;
|
|
private string PasswordInputType => showPassword ? "text" : "password";
|
|
private string PasswordIconClass => showPassword ? "mdi mdi-eye-off" : "mdi mdi-eye";
|
|
|
|
private enum FileSlot { Ca, Cert, Key }
|
|
|
|
private string? CaFileInfo;
|
|
private string? CaFileError;
|
|
|
|
private string? CertFileInfo;
|
|
private string? CertFileError;
|
|
|
|
private string? KeyFileInfo;
|
|
private string? KeyFileError;
|
|
|
|
private async Task OnFileSelected(InputFileChangeEventArgs e, FileSlot slot)
|
|
{
|
|
var file = e.File;
|
|
if (file is null)
|
|
return;
|
|
|
|
if (file.Size > MaxFileSize)
|
|
{
|
|
SetFileError(slot, $"File too large (max {FormatSize(MaxFileSize)})");
|
|
SetFileInfo(slot, string.Empty, 0);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var stream = file.OpenReadStream(MaxFileSize);
|
|
using var ms = new MemoryStream();
|
|
await stream.CopyToAsync(ms);
|
|
var data = ms.ToArray();
|
|
|
|
SetFileError(slot, null);
|
|
SetFileInfo(slot, file.Name, file.Size);
|
|
SetFileName(slot, file.Name);
|
|
SetBrowserFile(slot, file);
|
|
_ = ModelChanged.InvokeAsync(Model);
|
|
}
|
|
catch
|
|
{
|
|
SetFileError(slot, "Failed to read file");
|
|
}
|
|
}
|
|
|
|
private void RemoveFile(FileSlot slot)
|
|
{
|
|
SetFileInfo(slot, string.Empty, 0);
|
|
SetFileError(slot, null);
|
|
SetFileName(slot, string.Empty);
|
|
|
|
_ = ModelChanged.InvokeAsync(Model);
|
|
}
|
|
|
|
private void SetFileError(FileSlot slot, string? error)
|
|
{
|
|
switch (slot)
|
|
{
|
|
case FileSlot.Ca: CaFileError = error; break;
|
|
case FileSlot.Cert: CertFileError = error; break;
|
|
case FileSlot.Key: KeyFileError = error; break;
|
|
}
|
|
}
|
|
|
|
private void SetFileInfo(FileSlot slot, string? name, long size)
|
|
{
|
|
switch (slot)
|
|
{
|
|
case FileSlot.Ca: CaFileInfo = string.IsNullOrEmpty(name) ? "" : $"{name} {FormatSize(size)}"; break;
|
|
case FileSlot.Cert: CertFileInfo = string.IsNullOrEmpty(name) ? "" : $"{name} {FormatSize(size)}"; break;
|
|
case FileSlot.Key: KeyFileInfo = string.IsNullOrEmpty(name) ? "" : $"{name} {FormatSize(size)}"; break;
|
|
}
|
|
}
|
|
|
|
private void SetFileName(FileSlot slot, string? name)
|
|
{
|
|
switch (slot)
|
|
{
|
|
case FileSlot.Ca: Model.VDA5050CA = name; break;
|
|
case FileSlot.Cert: Model.VDA5050Cer = name; break;
|
|
case FileSlot.Key: Model.VDA5050Key = name; break;
|
|
}
|
|
}
|
|
|
|
private void SetBrowserFile(FileSlot slot, IBrowserFile file)
|
|
{
|
|
switch (slot)
|
|
{
|
|
case FileSlot.Ca: CaFile = file; break;
|
|
case FileSlot.Cert: CertFile = file; break;
|
|
case FileSlot.Key: KeyFile = file; break;
|
|
}
|
|
}
|
|
|
|
private static string FormatSize(long size)
|
|
{
|
|
if (size <= 0) return "0 B";
|
|
if (size < 1024) return $"{size} B";
|
|
double kb = size / 1024.0;
|
|
if (kb < 1024) return $"{kb:F1} KB";
|
|
double mb = kb / 1024.0;
|
|
return $"{mb:F2} MB";
|
|
}
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
if (EditContext is null || !EditContext.Model!.Equals(Model))
|
|
{
|
|
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
|
|
EditContext = new EditContext(Model);
|
|
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
|
|
CaFileInfo = string.Empty;
|
|
CertFileInfo = string.Empty;
|
|
KeyFileInfo = string.Empty;
|
|
}
|
|
}
|
|
|
|
private void TogglePasswordVisibility()
|
|
{
|
|
showPassword = !showPassword;
|
|
}
|
|
|
|
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
|
|
{
|
|
_ = ModelChanged.InvokeAsync(Model);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
|
|
}
|
|
}
|