feat(uns): scripted-alarm CRUD in IUnsTreeService for the equipment Alarms tab
This commit is contained in:
@@ -457,4 +457,59 @@ public interface IUnsTreeService
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>Success, a missing-row failure, or a concurrency failure.</returns>
|
||||
Task<(bool Ok, string? Error)> UpdateScriptSourceAsync(string scriptId, string sourceCode, byte[] rowVersion, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the scripted alarms owned by a single equipment as flat row projections for the equipment
|
||||
/// page's Alarms tab table, ordered by Name. Each row carries the display columns plus the
|
||||
/// <c>ScriptedAlarmId</c> the table uses to open the edit modal. Reads untracked. Returns an empty
|
||||
/// list when the equipment has no scripted alarms.
|
||||
/// </summary>
|
||||
/// <param name="equipmentId">The equipment whose scripted alarms to load.</param>
|
||||
/// <param name="ct">A token to cancel the load.</param>
|
||||
/// <returns>The equipment's scripted-alarm rows ordered by Name; empty if it has none.</returns>
|
||||
Task<IReadOnlyList<EquipmentAlarmRow>> LoadAlarmsForEquipmentAsync(string equipmentId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a single scripted alarm projected for editing, or <c>null</c> if it no longer exists.
|
||||
/// Reads untracked and captures the current concurrency token for last-write-wins saves.
|
||||
/// </summary>
|
||||
/// <param name="scriptedAlarmId">The scripted alarm to load.</param>
|
||||
/// <param name="ct">A token to cancel the load.</param>
|
||||
/// <returns>The scripted alarm's edit projection, or <c>null</c> when missing.</returns>
|
||||
Task<ScriptedAlarmEditDto?> LoadScriptedAlarmAsync(string scriptedAlarmId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new scripted alarm under an equipment. The <c>ScriptedAlarmId</c> is operator-typed
|
||||
/// (not system-generated). Fails on a duplicate <c>ScriptedAlarmId</c>. Field validation (severity
|
||||
/// range, required fields) is enforced client-side in the modal. On success the operator-typed id is
|
||||
/// echoed back via <see cref="UnsMutationResult.CreatedId"/>.
|
||||
/// </summary>
|
||||
/// <param name="equipmentId">The owning equipment.</param>
|
||||
/// <param name="input">The operator-editable scripted-alarm fields.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>Success carrying the created id, or a duplicate-id failure.</returns>
|
||||
Task<UnsMutationResult> CreateScriptedAlarmAsync(string equipmentId, ScriptedAlarmInput input, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a scripted alarm's mutable fields (name, alarm type, severity, message template,
|
||||
/// predicate script, and the historize/retain/enabled flags). The owning <c>EquipmentId</c> is
|
||||
/// preserved (the Alarms tab fixes it). Uses last-write-wins optimistic concurrency on
|
||||
/// <see cref="Configuration.Entities.ScriptedAlarm.RowVersion"/>.
|
||||
/// </summary>
|
||||
/// <param name="scriptedAlarmId">The scripted alarm to update.</param>
|
||||
/// <param name="input">The new operator-editable scripted-alarm fields.</param>
|
||||
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>Success, a missing-row failure, or a concurrency failure.</returns>
|
||||
Task<UnsMutationResult> UpdateScriptedAlarmAsync(string scriptedAlarmId, ScriptedAlarmInput input, byte[] rowVersion, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a scripted alarm. A missing row is treated as success (already gone). Uses last-write-wins
|
||||
/// optimistic concurrency on <see cref="Configuration.Entities.ScriptedAlarm.RowVersion"/>.
|
||||
/// </summary>
|
||||
/// <param name="scriptedAlarmId">The scripted alarm to delete.</param>
|
||||
/// <param name="rowVersion">The concurrency token the caller last read.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
||||
Task<UnsMutationResult> DeleteScriptedAlarmAsync(string scriptedAlarmId, byte[] rowVersion, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
/// <summary>A scripted-alarm row for the Alarms-tab table.</summary>
|
||||
public sealed record EquipmentAlarmRow(string ScriptedAlarmId, string Name, string AlarmType, int Severity, string PredicateScriptId, bool Enabled);
|
||||
|
||||
/// <summary>A scripted alarm projected for editing, with the concurrency token the modal echoes back.</summary>
|
||||
public sealed record ScriptedAlarmEditDto(
|
||||
string ScriptedAlarmId, string EquipmentId, string Name, string AlarmType, int Severity, string MessageTemplate,
|
||||
string PredicateScriptId, bool HistorizeToAveva, bool Retain, bool Enabled, byte[] RowVersion);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
/// <summary>Operator-editable fields for a scripted-alarm create/update on the equipment Alarms tab.
|
||||
/// The owning equipment is supplied separately (the tab fixes it), so there is no equipment field.</summary>
|
||||
public sealed record ScriptedAlarmInput(
|
||||
string ScriptedAlarmId, string Name, string AlarmType, int Severity, string MessageTemplate,
|
||||
string PredicateScriptId, bool HistorizeToAveva, bool Retain, bool Enabled);
|
||||
@@ -1286,4 +1286,82 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<EquipmentAlarmRow>> LoadAlarmsForEquipmentAsync(string equipmentId, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
return await db.ScriptedAlarms.AsNoTracking()
|
||||
.Where(a => a.EquipmentId == equipmentId)
|
||||
.OrderBy(a => a.Name)
|
||||
.Select(a => new EquipmentAlarmRow(a.ScriptedAlarmId, a.Name, a.AlarmType, a.Severity, a.PredicateScriptId, a.Enabled))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ScriptedAlarmEditDto?> LoadScriptedAlarmAsync(string scriptedAlarmId, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
return await db.ScriptedAlarms.AsNoTracking()
|
||||
.Where(a => a.ScriptedAlarmId == scriptedAlarmId)
|
||||
.Select(a => new ScriptedAlarmEditDto(a.ScriptedAlarmId, a.EquipmentId, a.Name, a.AlarmType, a.Severity,
|
||||
a.MessageTemplate, a.PredicateScriptId, a.HistorizeToAveva, a.Retain, a.Enabled, a.RowVersion))
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> CreateScriptedAlarmAsync(string equipmentId, ScriptedAlarmInput input, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
if (await db.ScriptedAlarms.AnyAsync(a => a.ScriptedAlarmId == input.ScriptedAlarmId, ct))
|
||||
return new UnsMutationResult(false, $"ScriptedAlarm '{input.ScriptedAlarmId}' already exists.");
|
||||
|
||||
db.ScriptedAlarms.Add(new ScriptedAlarm
|
||||
{
|
||||
ScriptedAlarmId = input.ScriptedAlarmId,
|
||||
EquipmentId = equipmentId,
|
||||
Name = input.Name,
|
||||
AlarmType = input.AlarmType,
|
||||
Severity = input.Severity,
|
||||
MessageTemplate = input.MessageTemplate,
|
||||
PredicateScriptId = input.PredicateScriptId,
|
||||
HistorizeToAveva = input.HistorizeToAveva,
|
||||
Retain = input.Retain,
|
||||
Enabled = input.Enabled,
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new UnsMutationResult(true, null, input.ScriptedAlarmId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> UpdateScriptedAlarmAsync(string scriptedAlarmId, ScriptedAlarmInput input, byte[] rowVersion, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var entity = await db.ScriptedAlarms.FirstOrDefaultAsync(a => a.ScriptedAlarmId == scriptedAlarmId, ct);
|
||||
if (entity is null) return new UnsMutationResult(false, "Row no longer exists.");
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
entity.Name = input.Name;
|
||||
entity.AlarmType = input.AlarmType;
|
||||
entity.Severity = input.Severity;
|
||||
entity.MessageTemplate = input.MessageTemplate;
|
||||
entity.PredicateScriptId = input.PredicateScriptId;
|
||||
entity.HistorizeToAveva = input.HistorizeToAveva;
|
||||
entity.Retain = input.Retain;
|
||||
entity.Enabled = input.Enabled;
|
||||
try { await db.SaveChangesAsync(ct); return new UnsMutationResult(true, null); }
|
||||
catch (DbUpdateConcurrencyException) { return new UnsMutationResult(false, "Another user changed this scripted alarm while you were editing."); }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> DeleteScriptedAlarmAsync(string scriptedAlarmId, byte[] rowVersion, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var entity = await db.ScriptedAlarms.FirstOrDefaultAsync(a => a.ScriptedAlarmId == scriptedAlarmId, ct);
|
||||
if (entity is null) return new UnsMutationResult(true, null);
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
db.ScriptedAlarms.Remove(entity);
|
||||
try { await db.SaveChangesAsync(ct); return new UnsMutationResult(true, null); }
|
||||
catch (DbUpdateConcurrencyException) { return new UnsMutationResult(false, "Another user changed this alarm while you were viewing it."); }
|
||||
catch (Exception ex) { return new UnsMutationResult(false, $"Delete failed: {ex.Message}."); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class UnsTreeServiceScriptedAlarmTests
|
||||
{
|
||||
private static UnsTreeService SeededService()
|
||||
{
|
||||
var dbName = $"uns-alarm-{Guid.NewGuid():N}";
|
||||
UnsTreeTestDb.SeedNamed(dbName);
|
||||
return new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||
}
|
||||
|
||||
private static ScriptedAlarmInput Sample(string id = "SA-1") =>
|
||||
new(id, "Over-temp", "LimitAlarm", 700, "{TagPath} hot", "SCRIPT-1", HistorizeToAveva: true, Retain: true, Enabled: true);
|
||||
|
||||
[Fact]
|
||||
public async Task Create_then_list_and_load_roundtrips()
|
||||
{
|
||||
var svc = SeededService();
|
||||
var create = await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample());
|
||||
create.Ok.ShouldBeTrue();
|
||||
create.CreatedId.ShouldBe("SA-1");
|
||||
|
||||
var rows = await svc.LoadAlarmsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
|
||||
rows.Count.ShouldBe(1);
|
||||
rows[0].Name.ShouldBe("Over-temp");
|
||||
rows[0].Severity.ShouldBe(700);
|
||||
|
||||
var dto = await svc.LoadScriptedAlarmAsync("SA-1");
|
||||
dto.ShouldNotBeNull();
|
||||
dto!.EquipmentId.ShouldBe(UnsTreeTestDb.SeededEquipmentId);
|
||||
dto.HistorizeToAveva.ShouldBeTrue();
|
||||
dto.RowVersion.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_rejects_duplicate_id()
|
||||
{
|
||||
var svc = SeededService();
|
||||
(await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample())).Ok.ShouldBeTrue();
|
||||
var dup = await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample());
|
||||
dup.Ok.ShouldBeFalse();
|
||||
dup.Error.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_changes_fields()
|
||||
{
|
||||
var svc = SeededService();
|
||||
await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample());
|
||||
var dto = await svc.LoadScriptedAlarmAsync("SA-1");
|
||||
|
||||
var upd = await svc.UpdateScriptedAlarmAsync("SA-1",
|
||||
Sample() with { Name = "Renamed", Severity = 250, HistorizeToAveva = false }, dto!.RowVersion);
|
||||
upd.Ok.ShouldBeTrue();
|
||||
|
||||
var after = await svc.LoadScriptedAlarmAsync("SA-1");
|
||||
after!.Name.ShouldBe("Renamed");
|
||||
after.Severity.ShouldBe(250);
|
||||
after.HistorizeToAveva.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_removes_row()
|
||||
{
|
||||
var svc = SeededService();
|
||||
await svc.CreateScriptedAlarmAsync(UnsTreeTestDb.SeededEquipmentId, Sample());
|
||||
var dto = await svc.LoadScriptedAlarmAsync("SA-1");
|
||||
|
||||
(await svc.DeleteScriptedAlarmAsync("SA-1", dto!.RowVersion)).Ok.ShouldBeTrue();
|
||||
(await svc.LoadAlarmsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId)).ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user