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:
@@ -18,328 +18,357 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small">Endpoint URL</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.EndpointUrl"
|
||||
placeholder="opc.tcp://host:4840" />
|
||||
@RenderFieldError("EndpointUrl")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Security Mode</label>
|
||||
<select class="form-select form-select-sm" @bind="Config.SecurityMode">
|
||||
<option value="@OpcUaSecurityMode.None">None</option>
|
||||
<option value="@OpcUaSecurityMode.Sign">Sign</option>
|
||||
<option value="@OpcUaSecurityMode.SignAndEncrypt">Sign & Encrypt</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="@($"{IdPrefix}-autoaccept")"
|
||||
@bind="Config.AutoAcceptUntrustedCerts" />
|
||||
<label class="form-check-label small"
|
||||
for="@($"{IdPrefix}-autoaccept")">Auto-accept certs</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-test="verify-endpoint-btn"
|
||||
disabled="@_verifying"
|
||||
@onclick="VerifyEndpoint">
|
||||
@if (_verifying)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
<span>Verifying…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Verify endpoint</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (_verifyResult is { } result)
|
||||
{
|
||||
@if (result.Success)
|
||||
{
|
||||
<span class="text-success small ms-2" data-test="verify-success">
|
||||
✓ Endpoint reachable
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-danger small ms-2" data-test="verify-failure">
|
||||
@result.FailureKind: @result.Error
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (result.FailureKind == VerifyFailureKind.UntrustedCertificate
|
||||
&& result.Cert is { } cert)
|
||||
{
|
||||
<div class="border rounded bg-body-secondary p-2 mt-2 small" data-test="verify-cert-panel">
|
||||
<div class="text-muted mb-1">Untrusted server certificate</div>
|
||||
<dl class="row mb-1 small">
|
||||
<dt class="col-sm-3">Subject</dt>
|
||||
<dd class="col-sm-9"><code>@cert.Subject</code></dd>
|
||||
<dt class="col-sm-3">Issuer</dt>
|
||||
<dd class="col-sm-9"><code>@cert.Issuer</code></dd>
|
||||
<dt class="col-sm-3">Thumbprint</dt>
|
||||
<dd class="col-sm-9"><code>@cert.Thumbprint</code></dd>
|
||||
<dt class="col-sm-3">Not before</dt>
|
||||
<dd class="col-sm-9">@cert.NotBeforeUtc.ToString("u")</dd>
|
||||
<dt class="col-sm-3">Not after</dt>
|
||||
<dd class="col-sm-9">@cert.NotAfterUtc.ToString("u")</dd>
|
||||
</dl>
|
||||
<AuthorizeView Policy="@AuthorizationPolicies.RequireAdmin">
|
||||
<Authorized>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm mt-1"
|
||||
data-test="trust-cert-btn"
|
||||
disabled="@_trusting"
|
||||
@onclick="() => TrustCert(cert)">
|
||||
@if (_trusting)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
<span>Trusting…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Trust certificate</span>
|
||||
}
|
||||
</button>
|
||||
<div class="text-muted fst-italic mt-1">
|
||||
Trusting adds this certificate to every node of the site's
|
||||
trusted-peer store (node-wide), then re-runs Verify.
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<div class="text-muted fst-italic">
|
||||
An Administrator must trust this certificate (cert management).
|
||||
</div>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
|
||||
@if (_trustError is { } trustError)
|
||||
{
|
||||
<div class="text-danger small mt-1" data-test="trust-cert-error">@trustError</div>
|
||||
}
|
||||
@if (_trustSucceeded)
|
||||
{
|
||||
<div class="text-success small mt-1" data-test="trust-cert-success">
|
||||
✓ Certificate trusted.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="text-muted small mt-2 mb-1">Authentication</div>
|
||||
@if (Config.UserIdentity is null)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2"
|
||||
@onclick="EnableAuthentication">Enable Authentication</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Each settings group gets a real boundary (fieldset + legend) rather than an
|
||||
unstyled muted caption. Fieldsets — not nested cards — because this editor is
|
||||
embedded inside DataConnectionForm's card; same idiom as TransportExport. *@
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h6">Connection</legend>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Token type</label>
|
||||
<select class="form-select form-select-sm" @bind="Config.UserIdentity.TokenType">
|
||||
<option value="@OpcUaUserTokenType.Anonymous">Anonymous</option>
|
||||
<option value="@OpcUaUserTokenType.UsernamePassword">Username / Password</option>
|
||||
<option value="@OpcUaUserTokenType.X509Certificate">X.509 Certificate</option>
|
||||
</select>
|
||||
</div>
|
||||
@if (Config.UserIdentity.TokenType == OpcUaUserTokenType.UsernamePassword)
|
||||
{
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Username</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.UserIdentity.Username" />
|
||||
@RenderFieldError("UserIdentity.Username")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Password</label>
|
||||
<input type="password" class="form-control form-control-sm"
|
||||
@bind="Config.UserIdentity.Password" />
|
||||
</div>
|
||||
}
|
||||
else if (Config.UserIdentity.TokenType == OpcUaUserTokenType.X509Certificate)
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Certificate path</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.UserIdentity.CertificatePath"
|
||||
placeholder="/etc/scadabridge/pki/client.pfx" />
|
||||
@RenderFieldError("UserIdentity.CertificatePath")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Certificate password</label>
|
||||
<input type="password" class="form-control form-control-sm"
|
||||
@bind="Config.UserIdentity.CertificatePassword" />
|
||||
</div>
|
||||
}
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
@onclick="() => Config.UserIdentity = null">
|
||||
Remove Authentication
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="text-muted small mt-2 mb-1">Timing</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Session timeout (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.SessionTimeoutMs" min="1" />
|
||||
@RenderFieldError("SessionTimeoutMs")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Operation timeout (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.OperationTimeoutMs" min="1" />
|
||||
@RenderFieldError("OperationTimeoutMs")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted small mt-2 mb-1">Subscription</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Publishing interval (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.PublishingIntervalMs" min="1" />
|
||||
@RenderFieldError("PublishingIntervalMs")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Sampling interval (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.SamplingIntervalMs" min="1" />
|
||||
@RenderFieldError("SamplingIntervalMs")
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Queue size</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.QueueSize" min="1" />
|
||||
@RenderFieldError("QueueSize")
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Keep-alive count</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.KeepAliveCount" min="1" />
|
||||
@RenderFieldError("KeepAliveCount")
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Lifetime count</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.LifetimeCount" min="1" />
|
||||
@RenderFieldError("LifetimeCount")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Max notifications / publish</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.MaxNotificationsPerPublish" min="1" />
|
||||
@RenderFieldError("MaxNotificationsPerPublish")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted small mt-2 mb-1">Advanced subscription</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Subscription display name</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.SubscriptionDisplayName" />
|
||||
@RenderFieldError("SubscriptionDisplayName")
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Subscription priority</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.SubscriptionPriority" min="0" max="255" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Timestamps to return</label>
|
||||
<select class="form-select form-select-sm" @bind="Config.TimestampsToReturn">
|
||||
<option value="@OpcUaTimestampsToReturn.Source">Source</option>
|
||||
<option value="@OpcUaTimestampsToReturn.Server">Server</option>
|
||||
<option value="@OpcUaTimestampsToReturn.Both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="@($"{IdPrefix}-discardoldest")"
|
||||
@bind="Config.DiscardOldest" />
|
||||
<label class="form-check-label small"
|
||||
for="@($"{IdPrefix}-discardoldest")">Discard oldest</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted small mt-2 mb-1">Deadband filter</div>
|
||||
@if (Config.Deadband is null)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2"
|
||||
@onclick="EnableDeadband">Enable Deadband</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Type</label>
|
||||
<select class="form-select form-select-sm" @bind="Config.Deadband.Type">
|
||||
<option value="@OpcUaDeadbandType.Absolute">Absolute</option>
|
||||
<option value="@OpcUaDeadbandType.Percent">Percent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Value</label>
|
||||
<input type="number" step="0.01" class="form-control form-control-sm"
|
||||
@bind="Config.Deadband.Value" min="0" />
|
||||
@RenderFieldError("Deadband.Value")
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
@onclick="() => Config.Deadband = null">
|
||||
Remove Deadband
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="text-muted small mt-2 mb-1">Heartbeat</div>
|
||||
@if (Config.Heartbeat is null)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2"
|
||||
@onclick="EnableHeartbeat">Enable Heartbeat</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Tag path</label>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small">Endpoint URL</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.Heartbeat.TagPath"
|
||||
placeholder="Sensors.Heartbeat" />
|
||||
@RenderFieldError("Heartbeat.TagPath")
|
||||
@bind="Config.EndpointUrl"
|
||||
placeholder="opc.tcp://host:4840" />
|
||||
@RenderFieldError("EndpointUrl")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Max silence (s)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.Heartbeat.MaxSilenceSeconds" min="1" />
|
||||
@RenderFieldError("Heartbeat.MaxSilenceSeconds")
|
||||
<label class="form-label small">Security Mode</label>
|
||||
<select class="form-select form-select-sm" @bind="Config.SecurityMode">
|
||||
<option value="@OpcUaSecurityMode.None">None</option>
|
||||
<option value="@OpcUaSecurityMode.Sign">Sign</option>
|
||||
<option value="@OpcUaSecurityMode.SignAndEncrypt">Sign & Encrypt</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
@onclick="() => Config.Heartbeat = null">
|
||||
Remove Heartbeat
|
||||
</button>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="@($"{IdPrefix}-autoaccept")"
|
||||
@bind="Config.AutoAcceptUntrustedCerts" />
|
||||
<label class="form-check-label small"
|
||||
for="@($"{IdPrefix}-autoaccept")">Auto-accept certs</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-test="verify-endpoint-btn"
|
||||
disabled="@_verifying"
|
||||
@onclick="VerifyEndpoint">
|
||||
@if (_verifying)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
<span>Verifying…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Verify endpoint</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (_verifyResult is { } result)
|
||||
{
|
||||
@if (result.Success)
|
||||
{
|
||||
<span class="text-success small ms-2" data-test="verify-success">
|
||||
✓ Endpoint reachable
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* The failure text is a raw driver exception message. Give it its own
|
||||
block below the button (rather than inline beside it) and clamp it,
|
||||
so a long message cannot reflow the whole button row. Full text
|
||||
stays reachable via the title. *@
|
||||
<div class="mt-1">
|
||||
<span class="text-danger small cell-clamp-2"
|
||||
data-test="verify-failure"
|
||||
title="@result.FailureKind: @result.Error">@result.FailureKind: @result.Error</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (result.FailureKind == VerifyFailureKind.UntrustedCertificate
|
||||
&& result.Cert is { } cert)
|
||||
{
|
||||
<div class="border rounded bg-body-secondary p-2 mt-2 small" data-test="verify-cert-panel">
|
||||
<div class="text-muted mb-1">Untrusted server certificate</div>
|
||||
@* Server-supplied X.509 DNs / thumbprints: clip so a long DN cannot
|
||||
stretch the panel; the full value stays in the title. *@
|
||||
<dl class="row mb-1 small">
|
||||
<dt class="col-sm-3">Subject</dt>
|
||||
<dd class="col-sm-9"><code class="cell-clip" title="@cert.Subject">@cert.Subject</code></dd>
|
||||
<dt class="col-sm-3">Issuer</dt>
|
||||
<dd class="col-sm-9"><code class="cell-clip" title="@cert.Issuer">@cert.Issuer</code></dd>
|
||||
<dt class="col-sm-3">Thumbprint</dt>
|
||||
<dd class="col-sm-9"><code class="cell-clip" title="@cert.Thumbprint">@cert.Thumbprint</code></dd>
|
||||
<dt class="col-sm-3">Not before</dt>
|
||||
<dd class="col-sm-9">@cert.NotBeforeUtc.ToString("u")</dd>
|
||||
<dt class="col-sm-3">Not after</dt>
|
||||
<dd class="col-sm-9">@cert.NotAfterUtc.ToString("u")</dd>
|
||||
</dl>
|
||||
<AuthorizeView Policy="@AuthorizationPolicies.RequireAdmin">
|
||||
<Authorized>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm mt-1"
|
||||
data-test="trust-cert-btn"
|
||||
disabled="@_trusting"
|
||||
@onclick="() => TrustCert(cert)">
|
||||
@if (_trusting)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
<span>Trusting…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Trust certificate</span>
|
||||
}
|
||||
</button>
|
||||
<div class="text-muted fst-italic mt-1">
|
||||
Trusting adds this certificate to every node of the site's
|
||||
trusted-peer store (node-wide), then re-runs Verify.
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<div class="text-muted fst-italic">
|
||||
An Administrator must trust this certificate (cert management).
|
||||
</div>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
|
||||
@if (_trustError is { } trustError)
|
||||
{
|
||||
@* Raw exception text from the trust call — clamped, full text in title. *@
|
||||
<div class="text-danger small mt-1 cell-clamp-2"
|
||||
data-test="trust-cert-error"
|
||||
title="@trustError">@trustError</div>
|
||||
}
|
||||
@if (_trustSucceeded)
|
||||
{
|
||||
<div class="text-success small mt-1" data-test="trust-cert-success">
|
||||
✓ Certificate trusted.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h6">Authentication</legend>
|
||||
@if (Config.UserIdentity is null)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2"
|
||||
@onclick="EnableAuthentication">Enable Authentication</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Token type</label>
|
||||
<select class="form-select form-select-sm" @bind="Config.UserIdentity.TokenType">
|
||||
<option value="@OpcUaUserTokenType.Anonymous">Anonymous</option>
|
||||
<option value="@OpcUaUserTokenType.UsernamePassword">Username / Password</option>
|
||||
<option value="@OpcUaUserTokenType.X509Certificate">X.509 Certificate</option>
|
||||
</select>
|
||||
</div>
|
||||
@if (Config.UserIdentity.TokenType == OpcUaUserTokenType.UsernamePassword)
|
||||
{
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Username</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.UserIdentity.Username" />
|
||||
@RenderFieldError("UserIdentity.Username")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Password</label>
|
||||
<input type="password" class="form-control form-control-sm"
|
||||
@bind="Config.UserIdentity.Password" />
|
||||
</div>
|
||||
}
|
||||
else if (Config.UserIdentity.TokenType == OpcUaUserTokenType.X509Certificate)
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Certificate path</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.UserIdentity.CertificatePath"
|
||||
placeholder="/etc/scadabridge/pki/client.pfx" />
|
||||
@RenderFieldError("UserIdentity.CertificatePath")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Certificate password</label>
|
||||
<input type="password" class="form-control form-control-sm"
|
||||
@bind="Config.UserIdentity.CertificatePassword" />
|
||||
</div>
|
||||
}
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
@onclick="() => Config.UserIdentity = null">
|
||||
Remove Authentication
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h6">Timing</legend>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Session timeout (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.SessionTimeoutMs" min="1" />
|
||||
@RenderFieldError("SessionTimeoutMs")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Operation timeout (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.OperationTimeoutMs" min="1" />
|
||||
@RenderFieldError("OperationTimeoutMs")
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h6">Subscription</legend>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Publishing interval (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.PublishingIntervalMs" min="1" />
|
||||
@RenderFieldError("PublishingIntervalMs")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Sampling interval (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.SamplingIntervalMs" min="1" />
|
||||
@RenderFieldError("SamplingIntervalMs")
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Queue size</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.QueueSize" min="1" />
|
||||
@RenderFieldError("QueueSize")
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Keep-alive count</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.KeepAliveCount" min="1" />
|
||||
@RenderFieldError("KeepAliveCount")
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Lifetime count</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.LifetimeCount" min="1" />
|
||||
@RenderFieldError("LifetimeCount")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Max notifications / publish</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.MaxNotificationsPerPublish" min="1" />
|
||||
@RenderFieldError("MaxNotificationsPerPublish")
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h6">Advanced subscription</legend>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Subscription display name</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.SubscriptionDisplayName" />
|
||||
@RenderFieldError("SubscriptionDisplayName")
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Subscription priority</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.SubscriptionPriority" min="0" max="255" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Timestamps to return</label>
|
||||
<select class="form-select form-select-sm" @bind="Config.TimestampsToReturn">
|
||||
<option value="@OpcUaTimestampsToReturn.Source">Source</option>
|
||||
<option value="@OpcUaTimestampsToReturn.Server">Server</option>
|
||||
<option value="@OpcUaTimestampsToReturn.Both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="@($"{IdPrefix}-discardoldest")"
|
||||
@bind="Config.DiscardOldest" />
|
||||
<label class="form-check-label small"
|
||||
for="@($"{IdPrefix}-discardoldest")">Discard oldest</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h6">Deadband filter</legend>
|
||||
@if (Config.Deadband is null)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2"
|
||||
@onclick="EnableDeadband">Enable Deadband</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Type</label>
|
||||
<select class="form-select form-select-sm" @bind="Config.Deadband.Type">
|
||||
<option value="@OpcUaDeadbandType.Absolute">Absolute</option>
|
||||
<option value="@OpcUaDeadbandType.Percent">Percent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Value</label>
|
||||
<input type="number" step="0.01" class="form-control form-control-sm"
|
||||
@bind="Config.Deadband.Value" min="0" />
|
||||
@RenderFieldError("Deadband.Value")
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
@onclick="() => Config.Deadband = null">
|
||||
Remove Deadband
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-0">
|
||||
<legend class="h6">Heartbeat</legend>
|
||||
@if (Config.Heartbeat is null)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2"
|
||||
@onclick="EnableHeartbeat">Enable Heartbeat</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Tag path</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@bind="Config.Heartbeat.TagPath"
|
||||
placeholder="Sensors.Heartbeat" />
|
||||
@RenderFieldError("Heartbeat.TagPath")
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Max silence (s)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="Config.Heartbeat.MaxSilenceSeconds" min="1" />
|
||||
@RenderFieldError("Heartbeat.MaxSilenceSeconds")
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
@onclick="() => Config.Heartbeat = null">
|
||||
Remove Heartbeat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
Reference in New Issue
Block a user