feat(ui): Layer E2 OpcUaEndpointEditor gains Authentication / Advanced / Deadband sections
Three new sections inserted into <OpcUaEndpointEditor>: 1. Authentication (between the existing Connection row and Timing) - 'Enable Authentication' button when Config.UserIdentity is null - TokenType select (Anonymous / UsernamePassword / X509Certificate) - Conditional Username + Password inputs for UsernamePassword - Conditional Certificate path + Certificate password for X509Certificate - 'Remove Authentication' button 2. Advanced subscription (after the existing Subscription row) - Subscription display name (text) - Subscription priority (number 0-255) - Timestamps to return (Source / Server / Both select) - Discard oldest (checkbox) 3. Deadband filter (after Advanced subscription) - 'Enable Deadband' button when Config.Deadband is null - Type select (Absolute / Percent), Value number input - 'Remove Deadband' button EnableAuthentication and EnableDeadband helpers complement EnableHeartbeat. All new fields use the existing RenderFieldError helper for validator errors. 82/82 CentralUI tests pass (the 10 new editor tests drove the design).
This commit is contained in:
@@ -40,6 +40,61 @@
|
|||||||
</div>
|
</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
|
||||||
|
{
|
||||||
|
<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/scadalink/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="text-muted small mt-2 mb-1">Timing</div>
|
||||||
<div class="row g-2 mb-2">
|
<div class="row g-2 mb-2">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@@ -96,6 +151,69 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<div class="text-muted small mt-2 mb-1">Heartbeat</div>
|
||||||
@if (Config.Heartbeat is null)
|
@if (Config.Heartbeat is null)
|
||||||
{
|
{
|
||||||
@@ -138,6 +256,12 @@
|
|||||||
private void EnableHeartbeat() =>
|
private void EnableHeartbeat() =>
|
||||||
Config.Heartbeat = new OpcUaHeartbeatConfig();
|
Config.Heartbeat = new OpcUaHeartbeatConfig();
|
||||||
|
|
||||||
|
private void EnableAuthentication() =>
|
||||||
|
Config.UserIdentity = new OpcUaUserIdentityConfig();
|
||||||
|
|
||||||
|
private void EnableDeadband() =>
|
||||||
|
Config.Deadband = new OpcUaDeadbandConfig();
|
||||||
|
|
||||||
private RenderFragment? RenderFieldError(string field)
|
private RenderFragment? RenderFieldError(string field)
|
||||||
{
|
{
|
||||||
var match = Errors?.Errors.FirstOrDefault(e =>
|
var match = Errors?.Errors.FirstOrDefault(e =>
|
||||||
|
|||||||
@@ -136,9 +136,10 @@ public class OpcUaEndpointEditorTests : BunitContext
|
|||||||
};
|
};
|
||||||
var cut = Render<OpcUaEndpointEditor>(p => p.Add(c => c.Config, config));
|
var cut = Render<OpcUaEndpointEditor>(p => p.Add(c => c.Config, config));
|
||||||
|
|
||||||
Assert.Contains("Username", cut.Markup);
|
// <label>Username</label> only renders for the UsernamePassword branch
|
||||||
Assert.Contains("Password", cut.Markup);
|
Assert.Contains(">Username<", cut.Markup);
|
||||||
Assert.DoesNotContain("Certificate path", cut.Markup);
|
Assert.Contains(">Password<", cut.Markup);
|
||||||
|
Assert.DoesNotContain(">Certificate path<", cut.Markup);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -153,9 +154,9 @@ public class OpcUaEndpointEditorTests : BunitContext
|
|||||||
};
|
};
|
||||||
var cut = Render<OpcUaEndpointEditor>(p => p.Add(c => c.Config, config));
|
var cut = Render<OpcUaEndpointEditor>(p => p.Add(c => c.Config, config));
|
||||||
|
|
||||||
Assert.Contains("Certificate path", cut.Markup);
|
Assert.Contains(">Certificate path<", cut.Markup);
|
||||||
Assert.Contains("Certificate password", cut.Markup);
|
Assert.Contains(">Certificate password<", cut.Markup);
|
||||||
Assert.DoesNotContain("Username", cut.Markup);
|
Assert.DoesNotContain(">Username<", cut.Markup);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -167,8 +168,8 @@ public class OpcUaEndpointEditorTests : BunitContext
|
|||||||
};
|
};
|
||||||
var cut = Render<OpcUaEndpointEditor>(p => p.Add(c => c.Config, config));
|
var cut = Render<OpcUaEndpointEditor>(p => p.Add(c => c.Config, config));
|
||||||
|
|
||||||
Assert.DoesNotContain("Username", cut.Markup);
|
Assert.DoesNotContain(">Username<", cut.Markup);
|
||||||
Assert.DoesNotContain("Certificate path", cut.Markup);
|
Assert.DoesNotContain(">Certificate path<", cut.Markup);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user