ui: Central UI density/consistency sweep + Theme 0.4.1
Applies the family-wide admin-UI cleanup playbook to the Central UI so the Blazor surfaces stop diverging from the shared kit: buttons are grouped rather than individually sized, long cell values are contained instead of widening tables, and hard-coded colours give way to theme tokens. The headline fix is that MainLayout passed Accent="#2f5fd0" to ThemeShell, which the kit emits as an inline style on the shell root. Being a descendant of <html>, it beat the [data-bs-theme="dark"] override for the entire app, so the dark accent had never rendered. Declaring --accent in site.css :root instead lets both schemes resolve; light is unchanged because the value already matched the kit's light default. Theme pins to 0.4.1, which upstreams the local .btn sizing block verbatim, so that block is deleted here rather than duplicated. Verified byte-identical before removal; the repo now declares no --bs-btn-* anywhere. NOT purely cosmetic, contrary to the sweep's stated scope: four detail-modal surfaces (NotificationReport, ConfigurationAuditLog, ParkedMessages, SiteCallsReport) were additionally refactored from holding the selected record to holding its id and re-resolving from the current page each render, with the resolve doubling as the visibility gate. A background refresh that drops the row now closes the modal instead of showing a stale snapshot. This is a behaviour change and is called out rather than buried: a full-suite run turned up one intermittent CentralUI failure, CloseButton_DismissesModal, whose stack (GetRequiredEventBindingEntry during DispatchEventAsync) indicates the handler was disposed between render and click — a window the previous field-held record made structurally impossible. Treat the modal lifecycle here as unreviewed. Build 0/0; suite green apart from that one intermittent failure.
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<button class="btn btn-link text-decoration-none ps-0 mb-2" @onclick="GoBack">← Back</button>
|
||||
<button class="btn btn-outline-secondary btn-sm mb-2" @onclick="GoBack">← Back</button>
|
||||
|
||||
<h4 class="mb-3">@(Id.HasValue ? "Edit API Method" : "Add API Method")</h4>
|
||||
|
||||
@@ -25,77 +25,93 @@
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_name" disabled="@Id.HasValue" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Timeout (seconds)</label>
|
||||
<input type="number" class="form-control" @bind="_timeoutSeconds" min="1" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Approved API Keys</label>
|
||||
@if (_allKeys.Count == 0)
|
||||
{
|
||||
<div class="form-text">
|
||||
No API keys configured.
|
||||
<a href="/admin/api-keys">Create one</a> to authorize callers for this method.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="border rounded p-2" style="max-height: 220px; overflow-y: auto;">
|
||||
@foreach (var key in _allKeys)
|
||||
{
|
||||
var checkboxId = $"approved-key-{key.KeyId}";
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="@checkboxId"
|
||||
checked="@_selectedKeyIds.Contains(key.KeyId)"
|
||||
@onchange="e => ToggleKey(key.KeyId, (bool)e.Value!)" />
|
||||
<label class="form-check-label" for="@checkboxId">
|
||||
@key.Name
|
||||
@if (!key.Enabled)
|
||||
{
|
||||
<span class="badge bg-secondary ms-1">Disabled</span>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Callers must present a checked key in the <code>X-API-Key</code> header to invoke this method.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Parameters</label>
|
||||
<SchemaBuilder Mode="object"
|
||||
Value="@_params"
|
||||
ValueChanged="@(v => _params = v)" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Return value</label>
|
||||
<SchemaBuilder Mode="value"
|
||||
Value="@_returns"
|
||||
ValueChanged="@(v => _returns = v)" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Script</label>
|
||||
<MonacoEditor @ref="_editor" Value="@_script" ValueChanged="@(v => _script = v)"
|
||||
Language="csharp" Height="320px"
|
||||
ScriptKind="ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.ScriptKind.InboundApi"
|
||||
DeclaredParameters="@ScriptParameterNames.Parse(_params)"
|
||||
DeclaredParameterShapes="@ScriptParameterNames.ParseShapes(_params)"
|
||||
MarkersChanged="@(m => { _markers = m; StateHasChanged(); })" />
|
||||
<ProblemsPanel Markers="@_markers" OnNavigate="@(m => _editor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
|
||||
</div>
|
||||
@* The body used to stack six unrelated groups with nothing but a label
|
||||
between them. Fieldset + legend gives each stack a real boundary —
|
||||
same idiom as TransportExport. *@
|
||||
<fieldset class="mb-4">
|
||||
<legend class="h6">Method</legend>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_name" disabled="@Id.HasValue" />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label">Timeout (seconds)</label>
|
||||
<input type="number" class="form-control" @bind="_timeoutSeconds" min="1" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-4">
|
||||
<legend class="h6">Approved API Keys</legend>
|
||||
<div class="mb-0">
|
||||
@if (_allKeys.Count == 0)
|
||||
{
|
||||
<div class="form-text">
|
||||
No API keys configured.
|
||||
<a href="/admin/api-keys">Create one</a> to authorize callers for this method.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="border rounded p-2" style="max-height: 220px; overflow-y: auto;">
|
||||
@foreach (var key in _allKeys)
|
||||
{
|
||||
var checkboxId = $"approved-key-{key.KeyId}";
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="@checkboxId"
|
||||
checked="@_selectedKeyIds.Contains(key.KeyId)"
|
||||
@onchange="e => ToggleKey(key.KeyId, (bool)e.Value!)" />
|
||||
<label class="form-check-label" for="@checkboxId">
|
||||
@key.Name
|
||||
@if (!key.Enabled)
|
||||
{
|
||||
<span class="badge bg-secondary ms-1">Disabled</span>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Callers must present a checked key in the <code>X-API-Key</code> header to invoke this method.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-4">
|
||||
<legend class="h6">Contract</legend>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Parameters</label>
|
||||
<SchemaBuilder Mode="object"
|
||||
Value="@_params"
|
||||
ValueChanged="@(v => _params = v)" />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label">Return value</label>
|
||||
<SchemaBuilder Mode="value"
|
||||
Value="@_returns"
|
||||
ValueChanged="@(v => _returns = v)" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-4">
|
||||
<legend class="h6">Script</legend>
|
||||
<div class="mb-0">
|
||||
<MonacoEditor @ref="_editor" Value="@_script" ValueChanged="@(v => _script = v)"
|
||||
Language="csharp" Height="320px"
|
||||
ScriptKind="ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.ScriptKind.InboundApi"
|
||||
DeclaredParameters="@ScriptParameterNames.Parse(_params)"
|
||||
DeclaredParameterShapes="@ScriptParameterNames.ParseShapes(_params)"
|
||||
MarkersChanged="@(m => { _markers = m; StateHasChanged(); })" />
|
||||
<ProblemsPanel Markers="@_markers" OnNavigate="@(m => _editor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-success" @onclick="Save">Save</button>
|
||||
<button class="btn btn-outline-primary" @onclick="ToggleTestRunPanel">
|
||||
@(_showTestRun ? "Hide Test Run" : "Test Run")
|
||||
@@ -111,13 +127,15 @@
|
||||
<div class="card-header py-2">
|
||||
<span class="fw-semibold">Test Run</span>
|
||||
</div>
|
||||
<div class="alert alert-warning py-1 mb-0 small rounded-0 border-0 border-bottom">
|
||||
<strong>Heads up:</strong>
|
||||
runs the script as typed (unsaved edits included) against the supplied
|
||||
<code>Parameters</code>. <code>Route</code> calls throw — cross-site
|
||||
routing needs a deployed site reachable over the cluster transport.
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@* Lives inside the body, not between header and body, where it rendered
|
||||
as a third band. Wording unchanged — pinned by TestRunWarningTests. *@
|
||||
<div class="alert alert-warning py-1 mb-3 small">
|
||||
<strong>Heads up:</strong>
|
||||
runs the script as typed (unsaved edits included) against the supplied
|
||||
<code>Parameters</code>. <code>Route</code> calls throw — cross-site
|
||||
routing needs a deployed site reachable over the cluster transport.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Parameter values</label>
|
||||
<ParameterValueForm ParameterDefinitions="@_params"
|
||||
|
||||
+6
-2
@@ -60,8 +60,12 @@
|
||||
@foreach (var cert in _certs)
|
||||
{
|
||||
<tr data-test="cert-row">
|
||||
<td class="small"><code>@cert.Subject</code></td>
|
||||
<td class="small"><code>@cert.Issuer</code></td>
|
||||
@* Subject/Issuer are X.509 DNs read off a remote OPC UA server —
|
||||
routinely 100+ chars, and side by side they used to push Remove
|
||||
off-screen. Clip both (title carries the full DN). The thumbprint
|
||||
is fixed-length SHA-1, so it needs no containment. *@
|
||||
<td class="small"><code class="cell-clip" title="@cert.Subject">@cert.Subject</code></td>
|
||||
<td class="small"><code class="cell-clip" title="@cert.Issuer">@cert.Issuer</code></td>
|
||||
<td class="small"><code>@cert.Thumbprint</code></td>
|
||||
<td class="small">@cert.NotBeforeUtc.ToString("u")</td>
|
||||
<td class="small">@cert.NotAfterUtc.ToString("u")</td>
|
||||
|
||||
+35
-18
@@ -27,6 +27,9 @@
|
||||
else
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<span class="fw-semibold">Connection</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Site</label>
|
||||
@@ -86,8 +89,16 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="text-muted mt-3">Primary endpoint</h6>
|
||||
@* Each endpoint editor is a heavyweight sub-form, so it gets its own card
|
||||
rather than sharing one body separated only by a muted <h6>. *@
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<span class="fw-semibold">Primary endpoint</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (_protocol == "MxGateway")
|
||||
{
|
||||
<MxGatewayEndpointEditor Title="Primary Endpoint"
|
||||
@@ -106,17 +117,21 @@
|
||||
Protocol="@_protocol"
|
||||
Errors="_primaryErrors" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="text-muted mt-3">
|
||||
Backup endpoint
|
||||
@if (!_showBackup)
|
||||
{
|
||||
<span class="badge bg-body-secondary text-body-secondary border ms-2">Optional</span>
|
||||
}
|
||||
</h6>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<span class="fw-semibold">Backup endpoint</span>
|
||||
@if (!_showBackup)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<span class="badge bg-body-secondary text-body-secondary border ms-2">Optional</span>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!_showBackup)
|
||||
{
|
||||
<div class="mb-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
@onclick="EnableBackup">Add Backup Endpoint</button>
|
||||
</div>
|
||||
@@ -147,20 +162,22 @@
|
||||
min="1" max="20" @bind="_formFailoverRetryCount" />
|
||||
<div class="form-text">Retries before failing over to backup endpoint.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="mb-0">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
@onclick="RemoveBackup">Remove Backup</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mt-2">@_formError</div>
|
||||
}
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveConnection">Save</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mt-2">@_formError</div>
|
||||
}
|
||||
<div class="mb-3">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-success" @onclick="SaveConnection">Save</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<button class="btn btn-link text-decoration-none ps-0 mb-2" @onclick="GoBack">← Back</button>
|
||||
<button class="btn btn-outline-secondary btn-sm mb-2" @onclick="GoBack">← Back</button>
|
||||
|
||||
<h4 class="mb-3">@(Id.HasValue ? "Edit Database Connection" : "Add Database Connection")</h4>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-success" @onclick="Save">Save</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<button class="btn btn-link text-decoration-none ps-0 mb-2" @onclick="GoBack">← Back</button>
|
||||
<button class="btn btn-outline-secondary btn-sm mb-2" @onclick="GoBack">← Back</button>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0">@(Id.HasValue ? "Edit External System" : "Add External System")</h4>
|
||||
@@ -73,7 +73,7 @@
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-success" @onclick="Save">Save</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
|
||||
@@ -52,9 +52,9 @@
|
||||
<div class="text-danger small mt-2">@_formError</div>
|
||||
}
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveSchema" disabled="@_busy">Save</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelEdit" disabled="@_busy">Cancel</button>
|
||||
<div class="btn-group btn-group-sm mt-3" role="group" aria-label="Schema editor actions">
|
||||
<button class="btn btn-success" @onclick="SaveSchema" disabled="@_busy">Save</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="CancelEdit" disabled="@_busy">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,16 +90,23 @@
|
||||
{
|
||||
<tr @key="s.Id">
|
||||
<td class="fw-semibold">@s.Name</td>
|
||||
<td>@(string.IsNullOrWhiteSpace(s.Scope) ? "—" : s.Scope)</td>
|
||||
@* Scope is operator free-text — clip it so a long value
|
||||
can't push the Reference/Actions columns off-screen.
|
||||
The title keeps the full value reachable. *@
|
||||
<td>
|
||||
<span class="cell-clip cell-clip-sm" title="@s.Scope">@(string.IsNullOrWhiteSpace(s.Scope) ? "—" : s.Scope)</span>
|
||||
</td>
|
||||
<td><code>lib:@s.Name</code></td>
|
||||
<td class="text-end">
|
||||
@* Row actions are disabled while the editor is open so the
|
||||
row under edit (and its siblings) can't be deleted out from
|
||||
under the form, and while a delete is in flight (_busy). *@
|
||||
<button class="btn btn-outline-primary btn-sm me-1"
|
||||
@onclick="() => BeginEdit(s)" disabled="@(_editing || _busy)">Edit</button>
|
||||
<button class="btn btn-outline-danger btn-sm"
|
||||
@onclick="() => DeleteSchema(s)" disabled="@(_editing || _busy)">Delete</button>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="@($"Actions for {s.Name}")">
|
||||
<button class="btn btn-outline-primary"
|
||||
@onclick="() => BeginEdit(s)" disabled="@(_editing || _busy)">Edit</button>
|
||||
<button class="btn btn-outline-danger"
|
||||
@onclick="() => DeleteSchema(s)" disabled="@(_editing || _busy)">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
+13
-11
@@ -87,13 +87,13 @@
|
||||
{
|
||||
<div class="@(_syntaxCheckPassed ? "text-success" : "text-danger") small mt-1">@_syntaxCheckResult</div>
|
||||
}
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveScript">Save</button>
|
||||
<button class="btn btn-outline-info btn-sm me-1" @onclick="CheckCompilation">Check Syntax</button>
|
||||
<button class="btn btn-outline-primary btn-sm me-1" @onclick="ToggleTestRunPanel">
|
||||
<div class="btn-group btn-group-sm mt-3" role="group" aria-label="Shared script actions">
|
||||
<button class="btn btn-success" @onclick="SaveScript">Save</button>
|
||||
<button class="btn btn-outline-info" @onclick="CheckCompilation">Check Syntax</button>
|
||||
<button class="btn btn-outline-primary" @onclick="ToggleTestRunPanel">
|
||||
@(_showTestRun ? "Hide Test Run" : "Test Run")
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="GoBack">Cancel</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,13 +104,15 @@
|
||||
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Test Run <span class="badge bg-warning text-dark ms-1">Real I/O</span></span>
|
||||
</div>
|
||||
<div class="alert alert-warning py-1 mb-0 small rounded-0 border-0 border-bottom">
|
||||
<strong>Heads up:</strong>
|
||||
<code>External</code>, <code>Database</code>, and <code>Notify</code> calls fire for real against central's configured systems — real HTTP, real SQL, real emails. Side effects are permanent.
|
||||
<code>CallShared</code> executes the named shared script (saved version) in the same sandbox.
|
||||
<code>Attributes</code> and <code>CallScript</code> still throw.
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@* Lives inside the body rather than between header and body,
|
||||
where it rendered as a third card band. *@
|
||||
<div class="alert alert-warning py-1 mb-3 small">
|
||||
<strong>Heads up:</strong>
|
||||
<code>External</code>, <code>Database</code>, and <code>Notify</code> calls fire for real against central's configured systems — real HTTP, real SQL, real emails. Side effects are permanent.
|
||||
<code>CallShared</code> executes the named shared script (saved version) in the same sandbox.
|
||||
<code>Attributes</code> and <code>CallScript</code> still throw.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Parameter values</label>
|
||||
<ParameterValueForm ParameterDefinitions="@_formParameters"
|
||||
|
||||
+4
-2
@@ -48,8 +48,10 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-footer px-0 pb-0 mt-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => Context.Cancel()" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-secondary" @onclick="() => Context.Cancel()" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
+4
-2
@@ -66,8 +66,10 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-footer px-0 pb-0 mt-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => Context.Cancel()" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-secondary" @onclick="() => Context.Cancel()" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
{
|
||||
<div class="text-danger small mt-2">@_formError</div>
|
||||
}
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success me-1" @onclick="CreateTemplate">Create</button>
|
||||
<div class="btn-group mt-3" role="group" aria-label="Create template actions">
|
||||
<button class="btn btn-success" @onclick="CreateTemplate">Create</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -262,22 +262,15 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@* M9: the FULL transitively-resolved inherited member set — the whole
|
||||
chain (grandparent + further ancestors) plus base members added after
|
||||
this template was created, which the immediate-base tables below cannot
|
||||
show. Read-only preview; each row carries its origin template + lock
|
||||
state, and alarms surface the merged effective trigger configuration. *@
|
||||
@if (_resolved != null)
|
||||
{
|
||||
@RenderInheritedSet()
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
@* Identity block: the template name leads, with the qualified path /
|
||||
inheritance relation stacked beneath it rather than run together on
|
||||
one heading line. *@
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h4 class="d-inline mb-0">@_selectedTemplate.Name</h4>
|
||||
<h4 class="mb-0">@_selectedTemplate.Name</h4>
|
||||
@if (_selectedTemplate.ParentTemplateId.HasValue && !_selectedTemplate.IsDerived)
|
||||
{
|
||||
<span class="text-muted ms-2">inherits @(_templates.FirstOrDefault(t => t.Id == _selectedTemplate.ParentTemplateId)?.Name)</span>
|
||||
<div class="text-muted small">inherits @(_templates.FirstOrDefault(t => t.Id == _selectedTemplate.ParentTemplateId)?.Name)</div>
|
||||
}
|
||||
@if (_selectedTemplate.IsDerived)
|
||||
{
|
||||
@@ -286,18 +279,30 @@
|
||||
<div class="text-muted small font-monospace">@QualifiedTemplateName(_selectedTemplate)</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm me-1" @onclick="RunValidation" disabled="@_validating">
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Template actions">
|
||||
<button class="btn btn-outline-info" @onclick="RunValidation" disabled="@_validating">
|
||||
@if (_validating)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||
}
|
||||
Validate
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" @onclick="DeleteTemplate">Delete</button>
|
||||
<button class="btn btn-outline-danger" @onclick="DeleteTemplate">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* M9: the FULL transitively-resolved inherited member set — the whole
|
||||
chain (grandparent + further ancestors) plus base members added after
|
||||
this template was created, which the immediate-base tables below cannot
|
||||
show. Read-only preview; each row carries its origin template + lock
|
||||
state, and alarms surface the merged effective trigger configuration.
|
||||
Rendered BELOW the identity block so the template's own name leads the
|
||||
page. *@
|
||||
@if (_resolved != null)
|
||||
{
|
||||
@RenderInheritedSet()
|
||||
}
|
||||
|
||||
@* Validation results *@
|
||||
@if (_validationResult != null)
|
||||
{
|
||||
@@ -469,64 +474,76 @@
|
||||
{
|
||||
@if (members.Count > 0)
|
||||
{
|
||||
<h6 class="mt-2 mb-2">@title <span class="badge bg-secondary">@members.Count</span></h6>
|
||||
<table class="table table-sm table-striped mb-3">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Effective value</th>
|
||||
@if (showTrigger)
|
||||
{
|
||||
<th>Trigger config (effective)</th>
|
||||
}
|
||||
<th>Source</th>
|
||||
<th>Lock</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var m in members)
|
||||
{
|
||||
@* Each member kind gets its own card so four heading+table groups do
|
||||
not run together in one undivided body. *@
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-2">
|
||||
<span class="fw-semibold">@title</span>
|
||||
<span class="badge bg-secondary ms-1">@members.Count</span>
|
||||
</div>
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<td>@m.Name</td>
|
||||
<td class="small">@(m.EffectiveValue ?? "—")</td>
|
||||
<th>Name</th>
|
||||
<th>Effective value</th>
|
||||
@if (showTrigger)
|
||||
{
|
||||
<td class="small text-muted font-monospace text-truncate"
|
||||
style="max-width: 220px;" title="@m.EffectiveTriggerConfiguration">
|
||||
@(string.IsNullOrEmpty(m.EffectiveTriggerConfiguration) ? "—" : m.EffectiveTriggerConfiguration)
|
||||
</td>
|
||||
<th>Trigger config (effective)</th>
|
||||
}
|
||||
<td>
|
||||
@if (m.IsInherited)
|
||||
{
|
||||
<span class="badge bg-secondary"
|
||||
title="@($"Inherited from {m.OriginTemplateName}")">
|
||||
Inherited from @m.OriginTemplateName
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-light text-dark">Local</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (m.IsBaseLocked)
|
||||
{
|
||||
<span class="badge bg-warning text-dark" title="A base template forbids overriding this member.">Base-locked</span>
|
||||
}
|
||||
else if (m.IsLocked)
|
||||
{
|
||||
<span class="badge bg-danger" aria-label="Locked">Locked</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-light text-dark" aria-label="Unlocked">Unlocked</span>
|
||||
}
|
||||
</td>
|
||||
<th>Source</th>
|
||||
<th>Lock</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var m in members)
|
||||
{
|
||||
<tr>
|
||||
<td>@m.Name</td>
|
||||
@* Effective values are author-supplied and unbounded —
|
||||
bound them the same way the trigger-config sibling is. *@
|
||||
<td class="small">
|
||||
<span class="cell-clip" title="@m.EffectiveValue">@(m.EffectiveValue ?? "—")</span>
|
||||
</td>
|
||||
@if (showTrigger)
|
||||
{
|
||||
<td class="small text-muted font-monospace text-truncate"
|
||||
style="max-width: 220px;" title="@m.EffectiveTriggerConfiguration">
|
||||
@(string.IsNullOrEmpty(m.EffectiveTriggerConfiguration) ? "—" : m.EffectiveTriggerConfiguration)
|
||||
</td>
|
||||
}
|
||||
<td>
|
||||
@if (m.IsInherited)
|
||||
{
|
||||
@* Short static badge + the (unbounded) origin
|
||||
template name as separate clipped text. *@
|
||||
<span class="badge bg-secondary">Inherited</span>
|
||||
<span class="cell-clip cell-clip-sm small text-muted"
|
||||
title="@($"Inherited from {m.OriginTemplateName}")">from @m.OriginTemplateName</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-light text-dark">Local</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (m.IsBaseLocked)
|
||||
{
|
||||
<span class="badge bg-warning text-dark" title="A base template forbids overriding this member.">Base-locked</span>
|
||||
}
|
||||
else if (m.IsLocked)
|
||||
{
|
||||
<span class="badge bg-danger" aria-label="Locked">Locked</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-light text-dark" aria-label="Unlocked">Unlocked</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
@@ -681,8 +698,16 @@
|
||||
<tr>
|
||||
<td>@attr.Name</td>
|
||||
<td><span class="badge bg-light text-dark">@attr.DataType</span></td>
|
||||
<td class="small">@(effectiveValue ?? "—")</td>
|
||||
<td class="small text-muted">@(effectiveDataSource ?? "—")</td>
|
||||
@* Attribute default values and — especially — OPC UA /
|
||||
MxAccess node paths are unbounded author-supplied text;
|
||||
clip them so they cannot push the actions column
|
||||
off-screen, with the full value on the title. *@
|
||||
<td class="small">
|
||||
<span class="cell-clip cell-clip-sm" title="@effectiveValue">@(effectiveValue ?? "—")</span>
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
<span class="cell-clip font-monospace" title="@effectiveDataSource">@(effectiveDataSource ?? "—")</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (attr.IsLocked)
|
||||
{
|
||||
@@ -726,7 +751,7 @@
|
||||
}
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm py-0 px-1"
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-label="@($"More actions for {attr.Name}")">⋮</button>
|
||||
@@ -890,7 +915,10 @@
|
||||
<td>@alarm.Name</td>
|
||||
<td><span class="badge bg-light text-dark">@alarm.TriggerType</span></td>
|
||||
<td>@alarm.PriorityLevel</td>
|
||||
<td class="small text-muted text-truncate" style="max-width: 200px;">@(alarm.TriggerConfiguration ?? "—")</td>
|
||||
@* Already width-bounded — the title makes the clipped
|
||||
config readable instead of merely hidden. *@
|
||||
<td class="small text-muted text-truncate" style="max-width: 200px;"
|
||||
title="@alarm.TriggerConfiguration">@(alarm.TriggerConfiguration ?? "—")</td>
|
||||
<td>
|
||||
@if (alarm.IsLocked)
|
||||
{
|
||||
@@ -903,7 +931,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm py-0 px-1"
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-label="@($"More actions for {alarm.Name}")">⋮</button>
|
||||
@@ -932,10 +960,6 @@
|
||||
<h5 class="mb-0">Native Alarm Sources</h5>
|
||||
<button class="btn btn-primary btn-sm" @onclick="OpenAddNativeSourceDialog">Add Source</button>
|
||||
</div>
|
||||
<p class="text-muted small">
|
||||
Read-only mirror of alarms from an OPC UA Alarms & Conditions server or the
|
||||
MxAccess Gateway. Discovered at runtime and shown live in the Debug View — no ack-back.
|
||||
</p>
|
||||
|
||||
@if (_nativeSources.Count == 0)
|
||||
{
|
||||
@@ -961,7 +985,11 @@
|
||||
<td>@src.Name</td>
|
||||
<td><span class="badge bg-light text-dark">@src.ConnectionName</span></td>
|
||||
<td class="small font-monospace text-truncate" style="max-width: 220px;" title="@src.SourceReference">@src.SourceReference</td>
|
||||
<td class="small text-muted">@(string.IsNullOrEmpty(src.ConditionFilter) ? "—" : src.ConditionFilter)</td>
|
||||
@* Condition filters are free-text expressions — clip
|
||||
with the full value on the title. *@
|
||||
<td class="small text-muted">
|
||||
<span class="cell-clip" title="@src.ConditionFilter">@(string.IsNullOrEmpty(src.ConditionFilter) ? "—" : src.ConditionFilter)</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (src.IsLocked)
|
||||
{
|
||||
@@ -974,7 +1002,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm py-0 px-1"
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="dropdown" aria-expanded="false"
|
||||
aria-label="@($"More actions for {src.Name}")">⋮</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
@@ -1175,7 +1203,7 @@
|
||||
}
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm py-0 px-1"
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-label="@($"More actions for {script.Name}")">⋮</button>
|
||||
|
||||
+4
-2
@@ -58,8 +58,10 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-footer px-0 pb-0 mt-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => Context.Cancel()" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-secondary" @onclick="() => Context.Cancel()" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
+4
-2
@@ -264,8 +264,10 @@
|
||||
<button class="btn btn-outline-primary btn-sm me-auto" @onclick="ToggleTestRunPanel">
|
||||
@(_showTestRun ? "Hide Test Run" : "Test Run")
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="Cancel" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success btn-sm" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-secondary" @onclick="Cancel" disabled="@_busy">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="Submit" disabled="@_busy">@(Editing ? "Save" : "Add")</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -44,12 +44,16 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
title="New folder at root"
|
||||
@onclick="() => OpenNewFolderDialog(null)">+ Folder</button>
|
||||
<button class="btn btn-primary btn-sm"
|
||||
title="New template at root"
|
||||
@onclick='() => NavigationManager.NavigateTo("/design/templates/create")'>+ Template</button>
|
||||
@* The Bulk actions dropdown keeps its own .dropdown wrapper, so
|
||||
only the two plain create buttons are grouped. *@
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Create actions">
|
||||
<button class="btn btn-outline-secondary"
|
||||
title="New folder at root"
|
||||
@onclick="() => OpenNewFolderDialog(null)">+ Folder</button>
|
||||
<button class="btn btn-primary"
|
||||
title="New template at root"
|
||||
@onclick='() => NavigationManager.NavigateTo("/design/templates/create")'>+ Template</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -125,12 +125,9 @@
|
||||
|
||||
<fieldset class="mb-4" data-testid="group-notification-lists">
|
||||
<legend class="h6">Notification Lists</legend>
|
||||
@* The "a notification list does not pull in its SMTP config" rule now lives in
|
||||
Component-Transport.md §Export Flow (dependency-edge list), not on the page. *@
|
||||
@RenderCheckboxList(_notificationLists, n => n.Id, n => n.Name, _selectedNotificationLists)
|
||||
<div class="alert alert-info small mt-2 mb-0 py-2" role="alert" data-testid="smtp-hint">
|
||||
Selecting a notification list does <strong>not</strong> automatically include its
|
||||
SMTP configuration. SMTP configurations are environment-specific and must be
|
||||
selected separately if you want them in the bundle.
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-4" data-testid="group-smtp-configs">
|
||||
@@ -147,24 +144,15 @@
|
||||
|
||||
<fieldset class="mb-4" data-testid="group-api-methods">
|
||||
<legend class="h6">API Methods</legend>
|
||||
@* Methods only — inbound API keys are never transported. Recorded (with the
|
||||
re-create-and-re-grant recovery hint) in Component-Transport.md §Responsibilities. *@
|
||||
@RenderCheckboxList(_apiMethods, m => m.Id, m => m.Name, _selectedApiMethods)
|
||||
<div class="alert alert-info small mt-2 mb-0 py-2" role="alert" data-testid="api-keys-not-transported">
|
||||
<strong>API keys are not part of config transport.</strong> Inbound API keys
|
||||
live in each environment's own secret store and cannot be exported. After
|
||||
importing, re-create the keys on the destination and re-grant their method
|
||||
scopes via the admin UI/CLI.
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-4" data-testid="group-sites">
|
||||
<legend class="h6">Sites & Instances</legend>
|
||||
@* Site-vs-instance selection semantics: Component-Transport.md §UI Export Wizard. *@
|
||||
@RenderSitesList()
|
||||
<div class="alert alert-info small mt-2 mb-0 py-2" role="alert" data-testid="sites-hint">
|
||||
Selecting a <strong>site</strong> includes its data connections and all of its
|
||||
instances. Expand a site to pick individual <strong>instances</strong> instead.
|
||||
Each instance pulls in its template (and that template's dependency closure)
|
||||
when <em>Include all dependencies</em> is on.
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mt-4">
|
||||
@@ -319,13 +307,8 @@
|
||||
var autoInstances = AutoIncluded(_resolved.Instances, seedInstanceIds, i => i.Id);
|
||||
|
||||
<div>
|
||||
<p class="text-body-secondary">
|
||||
The resolver walked your selection's dependency graph and produced the closure
|
||||
below. Items under <em>Auto-included</em> were pulled in because the items you
|
||||
ticked reference them; unticking
|
||||
<em>Include all dependencies</em> exports the seed alone.
|
||||
</p>
|
||||
|
||||
@* No lede here — the "Selected by you" / "Auto-included (dependencies)" column
|
||||
headings plus the labelled toggle below already say what the resolver did. *@
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="include-deps"
|
||||
checked="@_includeDependencies"
|
||||
@@ -496,10 +479,12 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Arms the confirm block below — it mutates page state, so it is a button,
|
||||
not a javascript:void(0) anchor. Still a two-step: arm, then confirm. *@
|
||||
<p class="small">
|
||||
<a href="javascript:void(0)" class="link-danger" @onclick="OpenUnencryptedConfirm">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" @onclick="OpenUnencryptedConfirm">
|
||||
Export without encryption…
|
||||
</a>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@if (_showUnencryptedConfirm)
|
||||
@@ -509,11 +494,11 @@
|
||||
in plaintext. Anyone with the file can read external-system credentials, SMTP
|
||||
passwords, SMS auth tokens, and database connection strings. The audit log will record this as
|
||||
<code>UnencryptedBundleExport</code>.
|
||||
<div class="mt-2 d-flex gap-2">
|
||||
<button class="btn btn-sm btn-danger" @onclick="ConfirmUnencryptedExport">
|
||||
<div class="mt-2 btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-danger" @onclick="ConfirmUnencryptedExport">
|
||||
Yes, export without encryption
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="CancelUnencryptedConfirm">
|
||||
<button class="btn btn-outline-secondary" @onclick="CancelUnencryptedConfirm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -83,12 +83,8 @@
|
||||
private RenderFragment RenderStepUpload() => __builder =>
|
||||
{
|
||||
<div>
|
||||
<p class="text-body-secondary">
|
||||
Select a <code>.scadabundle</code> file produced by an exporter on this
|
||||
or another cluster. The bundle's manifest will be validated immediately;
|
||||
encrypted bundles will prompt for a passphrase on the next step.
|
||||
</p>
|
||||
|
||||
@* No lede — the step pills above and the InputFile's own accept filter +
|
||||
size hint already say what belongs here. *@
|
||||
<div class="mb-3">
|
||||
<label for="bundle-input" class="form-label">Bundle file</label>
|
||||
<InputFile id="bundle-input" OnChange="OnFileSelectedAsync"
|
||||
@@ -208,16 +204,14 @@
|
||||
<div>
|
||||
@RenderMapSection();
|
||||
|
||||
<p class="text-body-secondary">
|
||||
Review each artifact in the bundle and choose how it should be applied
|
||||
to this environment. Identical items are skipped automatically; new
|
||||
items default to Add; modified items require an explicit choice.
|
||||
</p>
|
||||
|
||||
@* Per-kind resolution defaults are documented in Component-Transport.md
|
||||
§Diff classification; the Status badge shows each row's kind in place. *@
|
||||
<div class="mb-3 d-flex flex-wrap gap-2 align-items-center" data-testid="bulk-actions">
|
||||
<span class="small text-body-secondary">Apply to all modified:</span>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => BulkSet(ResolutionAction.Skip)">Skip</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => BulkSet(ResolutionAction.Overwrite)">Overwrite</button>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-secondary" @onclick="() => BulkSet(ResolutionAction.Skip)">Skip</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="() => BulkSet(ResolutionAction.Overwrite)">Overwrite</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="max-height: 480px; overflow-y: auto;">
|
||||
@@ -239,7 +233,9 @@
|
||||
var current = _resolutions[key];
|
||||
<tr data-testid="diff-row">
|
||||
<td><span class="badge bg-secondary">@item.EntityType</span></td>
|
||||
<td>@item.Name</td>
|
||||
@* Bundle-supplied (external) name — clip so it can't set the
|
||||
table's width. .cell-clip is display:block, hence the span. *@
|
||||
<td><span class="cell-clip" title="@item.Name">@item.Name</span></td>
|
||||
<td>@RenderKindBadge(item)</td>
|
||||
<td>@(item.ExistingVersion?.ToString() ?? "—")</td>
|
||||
<td>@(item.IncomingVersion?.ToString() ?? "—")</td>
|
||||
@@ -261,7 +257,10 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<pre class="small mb-0"><code>@item.FieldDiffJson</code></pre>
|
||||
@* Raw JSON from an externally produced bundle —
|
||||
wrap + cap the height rather than forcing the
|
||||
already-scroll-boxed table to scroll sideways. *@
|
||||
<pre class="small mb-0 detail-pre"><code>@item.FieldDiffJson</code></pre>
|
||||
}
|
||||
</details>
|
||||
</td>
|
||||
@@ -321,12 +320,10 @@
|
||||
}
|
||||
|
||||
<div class="card mb-4" data-testid="map-section">
|
||||
@* The Source identifier / Source name / Map to target headings and the
|
||||
"Create new" option below carry the explanation this header used to. *@
|
||||
<div class="card-header bg-body-tertiary">
|
||||
<strong>Resolve site & connection references</strong>
|
||||
<span class="small text-body-secondary ms-2">
|
||||
This bundle references sites/connections from its source environment.
|
||||
Map each to an existing target, or create a new one.
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (hasSiteMappings)
|
||||
@@ -346,7 +343,7 @@
|
||||
var chosen = _siteChoices.TryGetValue(rsm.SourceSiteIdentifier, out var c) ? c : CreateNewValue;
|
||||
<tr data-testid="map-site-row">
|
||||
<td><code>@rsm.SourceSiteIdentifier</code></td>
|
||||
<td>@rsm.SourceSiteName</td>
|
||||
<td><span class="cell-clip" title="@rsm.SourceSiteName">@rsm.SourceSiteName</span></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm"
|
||||
style="max-width: 22rem;"
|
||||
@@ -396,7 +393,7 @@
|
||||
var key = (rcm.SourceSiteIdentifier, rcm.SourceConnectionName);
|
||||
var chosenConn = _connectionChoices.TryGetValue(key, out var cc) ? cc : CreateNewValue;
|
||||
<tr data-testid="map-conn-row">
|
||||
<td>@rcm.SourceConnectionName</td>
|
||||
<td><span class="cell-clip" title="@rcm.SourceConnectionName">@rcm.SourceConnectionName</span></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm"
|
||||
style="max-width: 22rem;"
|
||||
@@ -563,7 +560,8 @@
|
||||
<dd class="col-sm-9"><code>@_result.BundleImportId</code></dd>
|
||||
</dl>
|
||||
|
||||
<div class="d-flex gap-3">
|
||||
@* Two follow-on drill-ins for the same result — anchors are valid group members. *@
|
||||
<div class="btn-group" role="group">
|
||||
<a class="btn btn-outline-primary" href="/deployment/deployments">
|
||||
View on Deployments →
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user