feat(adminui): inline script-source editor in the virtual-tag modal
This commit is contained in:
@@ -401,4 +401,36 @@ public interface IUnsTreeService
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>Success, a concurrency failure, or a delete-failed failure.</returns>
|
||||
Task<UnsMutationResult> DeleteVirtualTagAsync(string virtualTagId, byte[] rowVersion, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a script's editable source for the inline script-source panel in the virtual-tag modal,
|
||||
/// along with the concurrency token the panel must echo back on save and the script's display name.
|
||||
/// Reads untracked. Returns <c>null</c> when the script no longer exists.
|
||||
/// </summary>
|
||||
/// <param name="scriptId">The script whose source to load.</param>
|
||||
/// <param name="ct">A token to cancel the load.</param>
|
||||
/// <returns>The <c>(SourceCode, RowVersion, Name)</c> triple, or <c>null</c> when missing.</returns>
|
||||
Task<(string SourceCode, byte[] RowVersion, string Name)?> GetScriptSourceAsync(string scriptId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Counts how many virtual tags bind the given script, so the inline editor can warn the operator
|
||||
/// that an edit to a shared script affects every virtual tag using it.
|
||||
/// </summary>
|
||||
/// <param name="scriptId">The script to count usages of.</param>
|
||||
/// <param name="ct">A token to cancel the query.</param>
|
||||
/// <returns>The number of virtual tags whose <c>ScriptId</c> matches.</returns>
|
||||
Task<int> CountVirtualTagsUsingScriptAsync(string scriptId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves an edited script body from the inline panel: updates <c>SourceCode</c> and recomputes the
|
||||
/// SHA-256 <c>SourceHash</c> (lower-case hex, matching the Script-edit page). This save is separate
|
||||
/// from the virtual-tag save and is guarded by its own last-write-wins optimistic concurrency on
|
||||
/// <see cref="Configuration.Entities.Script.RowVersion"/>.
|
||||
/// </summary>
|
||||
/// <param name="scriptId">The script to update.</param>
|
||||
/// <param name="sourceCode">The new source body.</param>
|
||||
/// <param name="rowVersion">The concurrency token the panel 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<(bool Ok, string? Error)> UpdateScriptSourceAsync(string scriptId, string sourceCode, byte[] rowVersion, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
@@ -1014,6 +1016,67 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(string SourceCode, byte[] RowVersion, string Name)?> GetScriptSourceAsync(
|
||||
string scriptId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
var row = await db.Scripts
|
||||
.AsNoTracking()
|
||||
.Where(s => s.ScriptId == scriptId)
|
||||
.Select(s => new { s.SourceCode, s.RowVersion, s.Name })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
return row is null ? null : (row.SourceCode, row.RowVersion, row.Name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CountVirtualTagsUsingScriptAsync(string scriptId, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
return await db.VirtualTags.CountAsync(v => v.ScriptId == scriptId, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(bool Ok, string? Error)> UpdateScriptSourceAsync(
|
||||
string scriptId,
|
||||
string sourceCode,
|
||||
byte[] rowVersion,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
var entity = await db.Scripts.FirstOrDefaultAsync(s => s.ScriptId == scriptId, ct);
|
||||
if (entity is null)
|
||||
{
|
||||
return (false, "Script not found.");
|
||||
}
|
||||
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
|
||||
entity.SourceCode = sourceCode;
|
||||
entity.SourceHash = HashSource(sourceCode);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
return (true, null);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return (false, "This script was changed by someone else. Reload and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the SHA-256 of a script body as lower-case hex — the same algorithm the Script-edit
|
||||
/// page uses, so a body saved from either surface yields an identical compile-cache key.
|
||||
/// </summary>
|
||||
private static string HashSource(string source) =>
|
||||
Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(source)));
|
||||
|
||||
/// <summary>
|
||||
/// Validates a virtual tag's script binding and trigger configuration (mirrors the DbContext CHECK
|
||||
/// constraints): a script must be chosen, at least one of change-trigger / timer must be set, and a
|
||||
|
||||
Reference in New Issue
Block a user