410349767d
T34c only fixed the bounded modal-surface offenders. Bootstrap 5.3's bg-light
and bg-white are fixed light values that do NOT flip under [data-bs-theme=dark],
so every remaining use was a dark-mode contrast break. 35 swaps across 19 files:
surface / <pre> / <code> bg-light -> bg-body-secondary
panel bg-white -> bg-body
neutral badge bg-light text-dark -> bg-secondary-subtle text-secondary-emphasis
muted badge / input group bg-light text-muted -> bg-body-secondary text-body-secondary
DELIBERATELY LEFT (7 sites): the neutral member of a status-badge switch or
ternary whose siblings are all solid, non-theme-aware colours (bg-success,
bg-danger, bg-warning) — swapping only the neutral one to a subtle token breaks
the visual weight of the set, so these stay until the whole family is retoned:
Topology.razor:513 (Current badge) and :588 (InstanceState.NotDeployed)
InstanceConfigure.razor:1520 (same InstanceState switch)
NotificationReport.razor StatusBadgeClass fallback
TransportImport.razor ConflictKind badge fallback
SecuredWrites.razor text-bg-light fallback (a text-bg-* family with no
theme-aware member at all)
Health.razor:377 depth ternary
Also untouched by design: SchemaBuilder.razor:88 (already bg-light-subtle,
theme-aware), bg-dark text-light console panels, and site.css / #reconnect-modal.
InstanceConfigure.razor is edited concurrently elsewhere; its change here is 5
pure class-string swaps on existing lines, no reflow. No test asserted any of
these classes. Also marks all four M10 residuals done in the plan doc.
345 lines
17 KiB
Plaintext
345 lines
17 KiB
Plaintext
@page "/design/external-systems"
|
|
@using ZB.MOM.WW.ScadaBridge.Security
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi
|
|
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
|
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
|
@inject IExternalSystemRepository ExternalSystemRepository
|
|
@inject IInboundApiRepository InboundApiRepository
|
|
@inject NavigationManager NavigationManager
|
|
@inject IDialogService Dialog
|
|
|
|
<div class="container-fluid mt-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h4 class="mb-0">Integration Definitions</h4>
|
|
</div>
|
|
|
|
<ToastNotification @ref="_toast" />
|
|
|
|
@if (_loading)
|
|
{
|
|
<LoadingSpinner IsLoading="true" />
|
|
}
|
|
else if (_errorMessage != null)
|
|
{
|
|
<div class="alert alert-danger">@_errorMessage</div>
|
|
}
|
|
else
|
|
{
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link @(_tab == "extsys" ? "active" : "")"
|
|
role="tab"
|
|
aria-selected="@(_tab == "extsys" ? "true" : "false")"
|
|
aria-controls="int-tab-extsys"
|
|
@onclick='() => _tab = "extsys"'>
|
|
External Systems <span class="badge bg-secondary">@_externalSystems.Count</span>
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link @(_tab == "dbconn" ? "active" : "")"
|
|
role="tab"
|
|
aria-selected="@(_tab == "dbconn" ? "true" : "false")"
|
|
aria-controls="int-tab-dbconn"
|
|
@onclick='() => _tab = "dbconn"'>
|
|
Database Connections <span class="badge bg-secondary">@_dbConnections.Count</span>
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link @(_tab == "inbound" ? "active" : "")"
|
|
role="tab"
|
|
aria-selected="@(_tab == "inbound" ? "true" : "false")"
|
|
aria-controls="int-tab-inbound"
|
|
@onclick='() => _tab = "inbound"'>
|
|
Inbound API Methods <span class="badge bg-secondary">@_apiMethods.Count</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
@if (_tab == "extsys")
|
|
{
|
|
<div role="tabpanel" id="int-tab-extsys">@RenderExternalSystems()</div>
|
|
}
|
|
else if (_tab == "dbconn")
|
|
{
|
|
<div role="tabpanel" id="int-tab-dbconn">@RenderDbConnections()</div>
|
|
}
|
|
else if (_tab == "inbound")
|
|
{
|
|
<div role="tabpanel" id="int-tab-inbound">@RenderInboundApiMethods()</div>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
private bool _loading = true;
|
|
private string? _errorMessage;
|
|
private string _tab = "extsys";
|
|
|
|
// External Systems
|
|
private List<ExternalSystemDefinition> _externalSystems = new();
|
|
private string _extsysSearch = "";
|
|
private IEnumerable<ExternalSystemDefinition> FilteredExternalSystems =>
|
|
string.IsNullOrWhiteSpace(_extsysSearch)
|
|
? _externalSystems
|
|
: _externalSystems.Where(es => es.Name?.Contains(_extsysSearch, StringComparison.OrdinalIgnoreCase) ?? false);
|
|
|
|
// Database Connections
|
|
private List<DatabaseConnectionDefinition> _dbConnections = new();
|
|
private string _dbConnSearch = "";
|
|
private IEnumerable<DatabaseConnectionDefinition> FilteredDbConnections =>
|
|
string.IsNullOrWhiteSpace(_dbConnSearch)
|
|
? _dbConnections
|
|
: _dbConnections.Where(dc => dc.Name?.Contains(_dbConnSearch, StringComparison.OrdinalIgnoreCase) ?? false);
|
|
|
|
// Inbound API Methods
|
|
private List<ApiMethod> _apiMethods = new();
|
|
private string _apiMethodSearch = "";
|
|
private IEnumerable<ApiMethod> FilteredApiMethods =>
|
|
string.IsNullOrWhiteSpace(_apiMethodSearch)
|
|
? _apiMethods
|
|
: _apiMethods.Where(m => m.Name?.Contains(_apiMethodSearch, StringComparison.OrdinalIgnoreCase) ?? false);
|
|
|
|
private ToastNotification _toast = default!;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadAllAsync();
|
|
}
|
|
|
|
private async Task LoadAllAsync()
|
|
{
|
|
_loading = true;
|
|
try
|
|
{
|
|
_externalSystems = (await ExternalSystemRepository.GetAllExternalSystemsAsync()).ToList();
|
|
_dbConnections = (await ExternalSystemRepository.GetAllDatabaseConnectionsAsync()).ToList();
|
|
_apiMethods = (await InboundApiRepository.GetAllApiMethodsAsync()).ToList();
|
|
}
|
|
catch (Exception ex) { _errorMessage = ex.Message; }
|
|
_loading = false;
|
|
}
|
|
|
|
// ==== External Systems ====
|
|
private RenderFragment RenderExternalSystems() => __builder =>
|
|
{
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h5 class="mb-0">External Systems</h5>
|
|
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/external-systems/create")'>Add External System</button>
|
|
</div>
|
|
|
|
@if (_externalSystems.Count == 0)
|
|
{
|
|
<div class="text-center py-5 text-muted">
|
|
<p class="mb-3">No external systems configured.</p>
|
|
<button class="btn btn-primary btn-sm"
|
|
@onclick='() => NavigationManager.NavigateTo("/design/external-systems/create")'>
|
|
Add your first external system
|
|
</button>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="mb-3" style="max-width: 320px;">
|
|
<input class="form-control form-control-sm"
|
|
placeholder="Filter by name…"
|
|
@bind="_extsysSearch" @bind:event="oninput" />
|
|
</div>
|
|
|
|
@if (!FilteredExternalSystems.Any())
|
|
{
|
|
<p class="text-muted small">No external systems match the filter.</p>
|
|
}
|
|
|
|
<div class="row g-3">
|
|
@foreach (var es in FilteredExternalSystems)
|
|
{
|
|
<div class="col-lg-6 col-12" @key="es.Id">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<h5 class="card-title mb-0">@es.Name</h5>
|
|
<div class="d-flex gap-1">
|
|
<button class="btn btn-outline-primary btn-sm" @onclick='() => NavigationManager.NavigateTo($"/design/external-systems/{es.Id}/edit")'>Edit</button>
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary btn-sm"
|
|
data-bs-toggle="dropdown"
|
|
aria-expanded="false"
|
|
aria-label="@($"More actions for {es.Name}")">⋮</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><button class="dropdown-item text-danger" @onclick="() => DeleteExtSys(es)">Delete</button></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="small text-muted text-truncate mb-1" title="@es.EndpointUrl">@es.EndpointUrl</p>
|
|
<div>
|
|
<span class="badge bg-secondary me-1">@es.AuthType</span>
|
|
<span class="badge bg-secondary-subtle text-secondary-emphasis me-1">Max @es.MaxRetries retries</span>
|
|
<span class="badge bg-secondary-subtle text-secondary-emphasis">Delay @es.RetryDelay.TotalSeconds s</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
};
|
|
|
|
private async Task DeleteExtSys(ExternalSystemDefinition es)
|
|
{
|
|
if (!await Dialog.ConfirmAsync("Delete External System", $"Delete '{es.Name}'?", danger: true)) return;
|
|
try { await ExternalSystemRepository.DeleteExternalSystemAsync(es.Id); await ExternalSystemRepository.SaveChangesAsync(); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
|
catch (Exception ex) { _toast.ShowError(ex.Message); }
|
|
}
|
|
|
|
// ==== Database Connections ====
|
|
private RenderFragment RenderDbConnections() => __builder =>
|
|
{
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h5 class="mb-0">Database Connections</h5>
|
|
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/db-connections/create")'>Add Database Connection</button>
|
|
</div>
|
|
|
|
@if (_dbConnections.Count == 0)
|
|
{
|
|
<div class="text-center py-5 text-muted">
|
|
<p class="mb-3">No database connections configured.</p>
|
|
<button class="btn btn-primary btn-sm"
|
|
@onclick='() => NavigationManager.NavigateTo("/design/db-connections/create")'>
|
|
Add your first database connection
|
|
</button>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="mb-3" style="max-width: 320px;">
|
|
<input class="form-control form-control-sm"
|
|
placeholder="Filter by name…"
|
|
@bind="_dbConnSearch" @bind:event="oninput" />
|
|
</div>
|
|
|
|
@if (!FilteredDbConnections.Any())
|
|
{
|
|
<p class="text-muted small">No database connections match the filter.</p>
|
|
}
|
|
|
|
<div class="row g-3">
|
|
@foreach (var dc in FilteredDbConnections)
|
|
{
|
|
<div class="col-lg-6 col-12" @key="dc.Id">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<h5 class="card-title mb-0">@dc.Name</h5>
|
|
<div class="d-flex gap-1">
|
|
<button class="btn btn-outline-primary btn-sm" @onclick='() => NavigationManager.NavigateTo($"/design/db-connections/{dc.Id}/edit")'>Edit</button>
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary btn-sm"
|
|
data-bs-toggle="dropdown"
|
|
aria-expanded="false"
|
|
aria-label="@($"More actions for {dc.Name}")">⋮</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><button class="dropdown-item text-danger" @onclick="() => DeleteDbConn(dc)">Delete</button></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@* Connection strings carry credentials — never rendered here (not even for admins). Shown only on the create/edit form. *@
|
|
<p class="small text-muted fst-italic mb-1">Connection string hidden — edit to view</p>
|
|
<div>
|
|
<span class="badge bg-secondary-subtle text-secondary-emphasis me-1">Max @dc.MaxRetries retries</span>
|
|
<span class="badge bg-secondary-subtle text-secondary-emphasis">Delay @dc.RetryDelay.TotalSeconds s</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
};
|
|
|
|
private async Task DeleteDbConn(DatabaseConnectionDefinition dc)
|
|
{
|
|
if (!await Dialog.ConfirmAsync("Delete DB Connection", $"Delete '{dc.Name}'?", danger: true)) return;
|
|
try { await ExternalSystemRepository.DeleteDatabaseConnectionAsync(dc.Id); await ExternalSystemRepository.SaveChangesAsync(); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
|
catch (Exception ex) { _toast.ShowError(ex.Message); }
|
|
}
|
|
|
|
// ==== Inbound API Methods ====
|
|
private RenderFragment RenderInboundApiMethods() => __builder =>
|
|
{
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h5 class="mb-0">Inbound API Methods</h5>
|
|
<button class="btn btn-primary btn-sm" @onclick='() => NavigationManager.NavigateTo("/design/api-methods/create")'>Add API Method</button>
|
|
</div>
|
|
|
|
@if (_apiMethods.Count == 0)
|
|
{
|
|
<div class="text-center py-5 text-muted">
|
|
<p class="mb-3">No API methods configured.</p>
|
|
<button class="btn btn-primary btn-sm"
|
|
@onclick='() => NavigationManager.NavigateTo("/design/api-methods/create")'>
|
|
Add your first API method
|
|
</button>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="mb-3" style="max-width: 320px;">
|
|
<input class="form-control form-control-sm"
|
|
placeholder="Filter by name…"
|
|
@bind="_apiMethodSearch" @bind:event="oninput" />
|
|
</div>
|
|
|
|
@if (!FilteredApiMethods.Any())
|
|
{
|
|
<p class="text-muted small">No API methods match the filter.</p>
|
|
}
|
|
|
|
<div class="row g-3">
|
|
@foreach (var m in FilteredApiMethods)
|
|
{
|
|
var preview = m.Script.Length > 80 ? m.Script[..80] + "…" : m.Script;
|
|
<div class="col-lg-6 col-12" @key="m.Id">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<div>
|
|
<h5 class="card-title mb-1">@m.Name</h5>
|
|
<code class="small">POST /api/@m.Name</code>
|
|
</div>
|
|
<div class="d-flex gap-1">
|
|
<button class="btn btn-outline-primary btn-sm" @onclick='() => NavigationManager.NavigateTo($"/design/api-methods/{m.Id}/edit")'>Edit</button>
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary btn-sm"
|
|
data-bs-toggle="dropdown"
|
|
aria-expanded="false"
|
|
aria-label="@($"More actions for {m.Name}")">⋮</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><button class="dropdown-item text-danger" @onclick="() => DeleteApiMethod(m)">Delete</button></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<pre class="small text-muted font-monospace mb-2"
|
|
style="white-space: pre-wrap; word-break: break-all;"
|
|
title="@m.Script">@preview</pre>
|
|
<span class="badge bg-secondary-subtle text-secondary-emphasis">Timeout @m.TimeoutSeconds s</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
};
|
|
|
|
private async Task DeleteApiMethod(ApiMethod m)
|
|
{
|
|
if (!await Dialog.ConfirmAsync("Delete", $"Delete API method '{m.Name}'?", danger: true)) return;
|
|
try { await InboundApiRepository.DeleteApiMethodAsync(m.Id); await InboundApiRepository.SaveChangesAsync(); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
|
catch (Exception ex) { _toast.ShowError(ex.Message); }
|
|
}
|
|
|
|
}
|