RobotApp/RobotApp.Client/Pages/Components/Config/RobotVDA5050Config.razor
Đăng Nguyễn 8736bad3e7 update
2025-11-06 14:14:10 +07:00

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;
}
}