refactor(ui/design): card grid, SMTP split, TemplateEdit vertical-stack

Templates: <h4> in flex header, Expand/Collapse moved into a Bulk
actions dropdown, hover-visible kebab on tree nodes with aria-labels.
TreeView CSS gets a .tv-kebab opacity-on-hover utility.

TemplateCreate: form-control (not -sm) for primary inputs; accessible
Back button.

TemplateEdit: Properties card vertical-stacked with Save at the
bottom-right and Parent rendered as readonly plaintext. Add-member
forms (Attributes, Alarms, Scripts, Compositions) reflowed from
horizontal row g-2 align-items-end into cards with stacked col-12
inputs (Scripts gets rows=10). Lock/Unlock badges show full words.
Per-row Delete moved into a kebab dropdown. Tab nav gains
role="tablist" / role="tab" / aria-selected / aria-controls and panels
get role="tabpanel". Validation entries get consistent strong-and-
muted styling.

SharedScripts: migrated from table to card grid (col-lg-6) matching
Sites; cards show code preview + param/return badges + Edit + kebab.
Search filter, empty state CTA, @key.

SharedScriptForm: small ?-icon tooltips next to Parameters and Return
Definition labels.

ExternalSystems: SMTP split out to its own page; remaining tabs (
External Systems, DB Connections, Notification Lists, API Methods,
API Keys) unified as card grids with per-tab search + empty-state CTA.
Tab nav gets full ARIA instrumentation. Header gains a link to the
new SMTP page.

New page SmtpConfiguration.razor at /design/smtp: vertical-stacked
form using the existing Credentials field on the entity.

ExternalSystemForm: AuthConfig placeholder updates based on the
selected AuthType (None / ApiKey / BasicAuth).

DbConnectionForm: form-text below Connection String noting that the
value is stored in plain text and is admin-only.

ApiMethodForm: Script textarea rows=10; JSON example placeholders
for Params and Returns.

NotificationListForm: form-control sizing on Name/email inputs;
thead.table-dark -> table-light on the recipients table.
This commit is contained in:
Joseph Doherty
2026-05-12 03:32:39 -04:00
parent da2c0d714e
commit b6e2ec8a50
12 changed files with 1039 additions and 408 deletions

View File

@@ -0,0 +1,222 @@
@page "/design/smtp"
@using ScadaLink.Security
@using ScadaLink.Commons.Interfaces.Repositories
@using SmtpConfigurationEntity = ScadaLink.Commons.Entities.Notifications.SmtpConfiguration
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
@inject INotificationRepository NotificationRepository
@inject NavigationManager NavigationManager
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">SMTP Configuration</h4>
</div>
<ToastNotification @ref="_toast" />
<ConfirmDialog @ref="_confirmDialog" ConfirmButtonClass="btn-danger" />
@if (_loading)
{
<LoadingSpinner IsLoading="true" />
}
else if (_errorMessage != null)
{
<div class="alert alert-danger">@_errorMessage</div>
}
else
{
@if (_smtpConfigs.Count == 0 && !_showForm)
{
<div class="text-center py-5 text-muted">
<p class="mb-3">No SMTP configuration set.</p>
<button class="btn btn-primary btn-sm" @onclick="ShowAddForm">
Add SMTP configuration
</button>
</div>
}
else
{
@foreach (var smtp in _smtpConfigs)
{
<div class="card mb-3" @key="smtp.Id">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>@smtp.Host</strong>
@if (_editingSmtp?.Id != smtp.Id || !_showForm)
{
<button class="btn btn-outline-primary btn-sm" @onclick="() => StartEdit(smtp)">Edit</button>
}
</div>
<div class="card-body small">
<div class="row g-2">
<div class="col-md-4 text-muted">Host</div>
<div class="col-md-8">@smtp.Host:@smtp.Port</div>
<div class="col-md-4 text-muted">Auth Type</div>
<div class="col-md-8"><span class="badge bg-secondary">@smtp.AuthType</span></div>
<div class="col-md-4 text-muted">From Address</div>
<div class="col-md-8">@smtp.FromAddress</div>
<div class="col-md-4 text-muted">Credentials</div>
<div class="col-md-8">@(string.IsNullOrWhiteSpace(smtp.Credentials) ? "(not set)" : "(stored)")</div>
</div>
</div>
</div>
}
@if (_showForm)
{
<div class="card mb-3">
<div class="card-header">@(_editingSmtp != null ? "Edit SMTP Configuration" : "Add SMTP Configuration")</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Host</label>
<input type="text" class="form-control" @bind="_host" placeholder="smtp.example.com" />
</div>
<div class="col-md-4">
<label class="form-label">Port</label>
<input type="number" class="form-control" @bind="_port" min="1" max="65535" />
</div>
<div class="col-md-8">
<label class="form-label">Auth Type</label>
<select class="form-select" @bind="_authType">
<option>OAuth2</option>
<option>Basic</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Credentials</label>
<input type="password" class="form-control" @bind="_credentials"
placeholder="OAuth2 client secret or SMTP password" />
<div class="form-text">Treat as sensitive — visible to admins only.</div>
</div>
<div class="col-12">
<label class="form-label">From Address</label>
<input type="email" class="form-control" @bind="_fromAddress"
placeholder="noreply@example.com" />
</div>
@if (_formError != null)
{
<div class="col-12"><div class="text-danger small">@_formError</div></div>
}
<div class="col-12 text-end">
<button class="btn btn-outline-secondary me-1" @onclick="CancelForm">Cancel</button>
<button class="btn btn-success" @onclick="Save">Save</button>
</div>
</div>
</div>
</div>
}
else if (_smtpConfigs.Count == 0)
{
<button class="btn btn-primary btn-sm" @onclick="ShowAddForm">Add SMTP configuration</button>
}
}
}
</div>
@code {
private bool _loading = true;
private string? _errorMessage;
private List<SmtpConfigurationEntity> _smtpConfigs = new();
private bool _showForm;
private SmtpConfigurationEntity? _editingSmtp;
private string _host = string.Empty;
private int _port = 587;
private string _authType = "OAuth2";
private string? _credentials;
private string _fromAddress = string.Empty;
private string? _formError;
private ToastNotification _toast = default!;
private ConfirmDialog _confirmDialog = default!;
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task LoadAsync()
{
_loading = true;
_errorMessage = null;
try
{
_smtpConfigs = (await NotificationRepository.GetAllSmtpConfigurationsAsync()).ToList();
}
catch (Exception ex)
{
_errorMessage = ex.Message;
}
_loading = false;
}
private void ShowAddForm()
{
_editingSmtp = null;
_host = string.Empty;
_port = 587;
_authType = "OAuth2";
_credentials = null;
_fromAddress = string.Empty;
_formError = null;
_showForm = true;
}
private void StartEdit(SmtpConfigurationEntity smtp)
{
_editingSmtp = smtp;
_host = smtp.Host;
_port = smtp.Port;
_authType = smtp.AuthType;
_credentials = smtp.Credentials;
_fromAddress = smtp.FromAddress;
_formError = null;
_showForm = true;
}
private void CancelForm()
{
_showForm = false;
_formError = null;
}
private async Task Save()
{
_formError = null;
if (string.IsNullOrWhiteSpace(_host) || string.IsNullOrWhiteSpace(_fromAddress))
{
_formError = "Host and From Address are required.";
return;
}
try
{
if (_editingSmtp != null)
{
_editingSmtp.Host = _host.Trim();
_editingSmtp.Port = _port;
_editingSmtp.AuthType = _authType;
_editingSmtp.Credentials = _credentials?.Trim();
_editingSmtp.FromAddress = _fromAddress.Trim();
await NotificationRepository.UpdateSmtpConfigurationAsync(_editingSmtp);
}
else
{
var smtp = new SmtpConfigurationEntity(_host.Trim(), _authType, _fromAddress.Trim())
{
Port = _port,
Credentials = _credentials?.Trim()
};
await NotificationRepository.AddSmtpConfigurationAsync(smtp);
}
await NotificationRepository.SaveChangesAsync();
_showForm = false;
_toast.ShowSuccess("SMTP configuration saved.");
await LoadAsync();
}
catch (Exception ex)
{
_formError = ex.Message;
}
}
}