Gap 2 (#25): VirtualTagsTab.razor + /virtual-tags global page — list/create/toggle virtual tags per draft generation with DataType, Script, trigger, Historize, Enabled fields. Tab wired into DraftEditor. Gap 3 (#26): ScriptedAlarmsTab.razor + /scripted-alarms global page — list/create scripted alarms with AlarmType, Severity, MessageTemplate, PredicateScript, HistorizeToAveva, Retain. SeverityBand helper shows Low/Medium/High/Critical label. Tab wired into DraftEditor. Gap 4 (#27): ScriptLogHub (SignalR IAsyncEnumerable stream) tails scripts-*.log with optional ScriptName filter; ScriptLog.razor provides Start/Stop/Clear controls plus level filter dropdown. Hub registered at /hubs/script-log in Program.cs. Nav rail gains a "Scripting" eyebrow with entries for all three pages. 19 new unit tests for ScriptLogHub parse/filter/tail helpers (Category=Unit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,8 @@
|
||||
<li class="nav-item"><button class="nav-link @Active("drivers")" @onclick='() => _tab = "drivers"'>Drivers</button></li>
|
||||
<li class="nav-item"><button class="nav-link @Active("acls")" @onclick='() => _tab = "acls"'>ACLs</button></li>
|
||||
<li class="nav-item"><button class="nav-link @Active("scripts")" @onclick='() => _tab = "scripts"'>Scripts</button></li>
|
||||
<li class="nav-item"><button class="nav-link @Active("virtual-tags")" @onclick='() => _tab = "virtual-tags"'>Virtual Tags</button></li>
|
||||
<li class="nav-item"><button class="nav-link @Active("scripted-alarms")" @onclick='() => _tab = "scripted-alarms"'>Scripted Alarms</button></li>
|
||||
</ul>
|
||||
|
||||
<div class="row">
|
||||
@@ -49,6 +51,8 @@
|
||||
else if (_tab == "drivers") { <DriversTab GenerationId="@GenerationId" ClusterId="@ClusterId"/> }
|
||||
else if (_tab == "acls") { <AclsTab GenerationId="@GenerationId" ClusterId="@ClusterId"/> }
|
||||
else if (_tab == "scripts") { <ScriptsTab GenerationId="@GenerationId" ClusterId="@ClusterId"/> }
|
||||
else if (_tab == "virtual-tags") { <VirtualTagsTab GenerationId="@GenerationId" ClusterId="@ClusterId"/> }
|
||||
else if (_tab == "scripted-alarms") { <ScriptedAlarmsTab GenerationId="@GenerationId" ClusterId="@ClusterId"/> }
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<section class="panel rise sticky-top" style="animation-delay:.02s">
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||
@inject ScriptedAlarmService AlarmSvc
|
||||
@inject ScriptService ScriptSvc
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="panel-head mb-0">Scripted Alarms</h4>
|
||||
<small class="text-muted">OPC UA Part 9 alarms raised by C# predicate scripts. Additive to driver-native alarm streams.</small>
|
||||
</div>
|
||||
<button class="btn btn-primary" @onclick="StartNew">+ New alarm</button>
|
||||
</div>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<p class="text-muted">Loading…</p>
|
||||
}
|
||||
else if (_alarms.Count == 0 && !_showForm)
|
||||
{
|
||||
<section class="panel notice rise" style="animation-delay:.02s">No scripted alarms yet in this draft.</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (_alarms.Count > 0)
|
||||
{
|
||||
<section class="panel rise" style="animation-delay:.02s">
|
||||
<div class="panel-head d-flex justify-content-between align-items-center">
|
||||
<span>Scripted alarms in draft gen @GenerationId</span>
|
||||
<span class="tb-count text-muted">@_alarms.Count alarm@(_alarms.Count == 1 ? "" : "s")</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Equipment</th>
|
||||
<th>Type</th>
|
||||
<th class="num">Severity</th>
|
||||
<th>Predicate script</th>
|
||||
<th>Historize</th>
|
||||
<th>Retain</th>
|
||||
<th>Enabled</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var a in _alarms)
|
||||
{
|
||||
<tr>
|
||||
<td><span class="mono">@a.Name</span></td>
|
||||
<td><span class="mono">@a.EquipmentId</span></td>
|
||||
<td><span class="chip chip-idle">@a.AlarmType</span></td>
|
||||
<td class="num">@a.Severity <small class="text-muted">@SeverityBand(a.Severity)</small></td>
|
||||
<td><span class="mono">@(ScriptName(a.PredicateScriptId))</span></td>
|
||||
<td>
|
||||
@if (a.HistorizeToAveva) { <span class="chip chip-ok">Aveva</span> }
|
||||
else { <span class="text-muted">—</span> }
|
||||
</td>
|
||||
<td>
|
||||
@if (a.Retain) { <span class="chip chip-ok">yes</span> }
|
||||
else { <span class="text-muted">—</span> }
|
||||
</td>
|
||||
<td>
|
||||
@if (a.Enabled) { <span class="chip chip-ok">enabled</span> }
|
||||
else { <span class="chip chip-idle">disabled</span> }
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAsync(a.ScriptedAlarmId)">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@if (_showForm)
|
||||
{
|
||||
<section class="panel rise mt-3" style="animation-delay:.08s">
|
||||
<div class="panel-head d-flex justify-content-between align-items-center">
|
||||
<strong>New scripted alarm</strong>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => _showForm = false">Cancel</button>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Equipment ID</label>
|
||||
<input class="form-control form-control-sm" @bind="_draft.EquipmentId"
|
||||
placeholder="e.g. eq-abc123 — logical FK to Equipment"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Alarm name <small class="text-muted">(operator-facing display name)</small></label>
|
||||
<input class="form-control form-control-sm" @bind="_draft.Name"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Alarm type <small class="text-muted">(OPC UA Part 9 subtype)</small></label>
|
||||
<select class="form-select form-select-sm" @bind="_draft.AlarmType">
|
||||
@foreach (var t in AlarmTypes)
|
||||
{
|
||||
<option value="@t">@t</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">
|
||||
Severity <small class="text-muted">1–1000 (Low <250, Med <500, High <750, Critical 1000)</small>
|
||||
</label>
|
||||
<input type="number" min="1" max="1000" class="form-control form-control-sm" @bind="_draft.Severity"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Predicate script <small class="text-muted">(returns bool)</small></label>
|
||||
<select class="form-select form-select-sm" @bind="_draft.PredicateScriptId">
|
||||
<option value="">— select script —</option>
|
||||
@foreach (var s in _scripts)
|
||||
{
|
||||
<option value="@s.ScriptId">@s.Name (@s.ScriptId)</option>
|
||||
}
|
||||
</select>
|
||||
@if (_scripts.Count == 0)
|
||||
{
|
||||
<div class="form-text s-warn">No scripts in this draft — create one in the Scripts tab first.</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">
|
||||
Message template
|
||||
<small class="text-muted">Use <code class="mono">{EquipmentPath/TagName}</code> tokens — resolved at alarm emission time</small>
|
||||
</label>
|
||||
<input class="form-control form-control-sm" @bind="_draft.MessageTemplate"
|
||||
placeholder='e.g. "Oven {Plant/Line1/Oven/Temp} exceeds limit {Plant/Line1/Oven/TempLimit}"'/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check mt-2">
|
||||
<input type="checkbox" class="form-check-input" id="salHistorize" @bind="_draft.HistorizeToAveva"/>
|
||||
<label class="form-check-label" for="salHistorize">
|
||||
Historize to Aveva <small class="text-muted">(SQLite store-and-forward sink)</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check mt-2">
|
||||
<input type="checkbox" class="form-check-input" id="salRetain" @bind="_draft.Retain"/>
|
||||
<label class="form-check-label" for="salRetain">
|
||||
Retain <small class="text-muted">(keep condition visible after clear while un-acked)</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_error is not null)
|
||||
{
|
||||
<section class="panel notice mt-3"><span class="s-bad">@_error</span></section>
|
||||
}
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-primary" disabled="@_busy" @onclick="SaveAsync">Save</button>
|
||||
<button class="btn btn-sm btn-secondary ms-2" @onclick="() => _showForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public long GenerationId { get; set; }
|
||||
[Parameter] public string ClusterId { get; set; } = string.Empty;
|
||||
|
||||
private static readonly string[] AlarmTypes =
|
||||
["AlarmCondition", "LimitAlarm", "OffNormalAlarm", "DiscreteAlarm"];
|
||||
|
||||
private bool _loading = true;
|
||||
private bool _busy;
|
||||
private bool _showForm;
|
||||
private List<ScriptedAlarm> _alarms = [];
|
||||
private List<Script> _scripts = [];
|
||||
private string? _error;
|
||||
|
||||
private ScriptedAlarm _draft = NewDraft();
|
||||
|
||||
private static ScriptedAlarm NewDraft() => new()
|
||||
{
|
||||
ScriptedAlarmId = string.Empty,
|
||||
EquipmentId = string.Empty,
|
||||
Name = string.Empty,
|
||||
AlarmType = "AlarmCondition",
|
||||
Severity = 500,
|
||||
MessageTemplate = string.Empty,
|
||||
PredicateScriptId = string.Empty,
|
||||
HistorizeToAveva = true,
|
||||
Retain = true,
|
||||
};
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_loading = true;
|
||||
_alarms = await AlarmSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
_scripts = await ScriptSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private void StartNew()
|
||||
{
|
||||
_draft = NewDraft();
|
||||
_error = null;
|
||||
_showForm = true;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_draft.EquipmentId) ||
|
||||
string.IsNullOrWhiteSpace(_draft.Name) ||
|
||||
string.IsNullOrWhiteSpace(_draft.PredicateScriptId))
|
||||
{
|
||||
_error = "Equipment ID, Name, and Predicate script are required.";
|
||||
return;
|
||||
}
|
||||
if (_draft.Severity is < 1 or > 1000)
|
||||
{
|
||||
_error = "Severity must be between 1 and 1000.";
|
||||
return;
|
||||
}
|
||||
|
||||
_busy = true;
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
await AlarmSvc.AddAsync(
|
||||
GenerationId,
|
||||
_draft.EquipmentId, _draft.Name, _draft.AlarmType,
|
||||
_draft.Severity, _draft.MessageTemplate, _draft.PredicateScriptId,
|
||||
_draft.HistorizeToAveva, _draft.Retain,
|
||||
CancellationToken.None);
|
||||
_showForm = false;
|
||||
_alarms = await AlarmSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex) { _error = ex.Message; }
|
||||
finally { _busy = false; }
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(string id)
|
||||
{
|
||||
await AlarmSvc.DeleteAsync(GenerationId, id, CancellationToken.None);
|
||||
_alarms = await AlarmSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
}
|
||||
|
||||
private string ScriptName(string scriptId)
|
||||
{
|
||||
var s = _scripts.FirstOrDefault(x => x.ScriptId == scriptId);
|
||||
return s is not null ? s.Name : scriptId;
|
||||
}
|
||||
|
||||
private static string SeverityBand(int s) => s switch
|
||||
{
|
||||
<= 250 => "(Low)",
|
||||
<= 500 => "(Medium)",
|
||||
<= 750 => "(High)",
|
||||
_ => "(Critical)",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||
@inject VirtualTagService VirtualTagSvc
|
||||
@inject ScriptService ScriptSvc
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="panel-head mb-0">Virtual Tags</h4>
|
||||
<small class="text-muted">Computed tags driven by C# scripts. Appear in the Equipment browse tree alongside driver tags.</small>
|
||||
</div>
|
||||
<button class="btn btn-primary" @onclick="StartNew">+ New virtual tag</button>
|
||||
</div>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<p class="text-muted">Loading…</p>
|
||||
}
|
||||
else if (_tags.Count == 0 && !_showForm)
|
||||
{
|
||||
<section class="panel notice rise" style="animation-delay:.02s">No virtual tags yet in this draft.</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (_tags.Count > 0)
|
||||
{
|
||||
<section class="panel rise" style="animation-delay:.02s">
|
||||
<div class="panel-head d-flex justify-content-between align-items-center">
|
||||
<span>Virtual tags in draft gen @GenerationId</span>
|
||||
<span class="tb-count text-muted">@_tags.Count tag@(_tags.Count == 1 ? "" : "s")</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Equipment</th>
|
||||
<th>DataType</th>
|
||||
<th>Script</th>
|
||||
<th>Triggers</th>
|
||||
<th>Historize</th>
|
||||
<th>Enabled</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var t in _tags)
|
||||
{
|
||||
<tr>
|
||||
<td><span class="mono">@t.Name</span></td>
|
||||
<td><span class="mono">@t.EquipmentId</span></td>
|
||||
<td>@t.DataType</td>
|
||||
<td><span class="mono">@(ScriptName(t.ScriptId))</span></td>
|
||||
<td>
|
||||
@if (t.ChangeTriggered) { <span class="chip chip-idle me-1">change</span> }
|
||||
@if (t.TimerIntervalMs.HasValue) { <span class="chip chip-idle">@t.TimerIntervalMs ms</span> }
|
||||
</td>
|
||||
<td>
|
||||
@if (t.Historize) { <span class="chip chip-ok">yes</span> }
|
||||
else { <span class="text-muted">—</span> }
|
||||
</td>
|
||||
<td>
|
||||
@if (t.Enabled) { <span class="chip chip-ok">enabled</span> }
|
||||
else { <span class="chip chip-idle">disabled</span> }
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" @onclick="() => ToggleEnabledAsync(t)">
|
||||
@(t.Enabled ? "Disable" : "Enable")
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAsync(t.VirtualTagId)">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@if (_showForm)
|
||||
{
|
||||
<section class="panel rise mt-3" style="animation-delay:.08s">
|
||||
<div class="panel-head d-flex justify-content-between align-items-center">
|
||||
<strong>New virtual tag</strong>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => _showForm = false">Cancel</button>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Equipment ID</label>
|
||||
<input class="form-control form-control-sm" @bind="_draft.EquipmentId"
|
||||
placeholder="e.g. eq-abc123 — logical FK to Equipment"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Name <small class="text-muted">(browse name, unique in Equipment)</small></label>
|
||||
<input class="form-control form-control-sm" @bind="_draft.Name"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">DataType</label>
|
||||
<select class="form-select form-select-sm" @bind="_draft.DataType">
|
||||
@foreach (var dt in DataTypes)
|
||||
{
|
||||
<option value="@dt">@dt</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Script</label>
|
||||
<select class="form-select form-select-sm" @bind="_draft.ScriptId">
|
||||
<option value="">— select script —</option>
|
||||
@foreach (var s in _scripts)
|
||||
{
|
||||
<option value="@s.ScriptId">@s.Name (@s.ScriptId)</option>
|
||||
}
|
||||
</select>
|
||||
@if (_scripts.Count == 0)
|
||||
{
|
||||
<div class="form-text s-warn">No scripts in this draft — create one in the Scripts tab first.</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mt-2">
|
||||
<input type="checkbox" class="form-check-input" id="vtChangeTriggered" @bind="_draft.ChangeTriggered"/>
|
||||
<label class="form-check-label" for="vtChangeTriggered">
|
||||
Change-triggered <small class="text-muted">(re-evaluate on any input change)</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Timer interval (ms) <small class="text-muted">leave blank to disable timer</small></label>
|
||||
<input type="number" class="form-control form-control-sm" @bind="_timerMs" placeholder="e.g. 5000"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="vtHistorize" @bind="_draft.Historize"/>
|
||||
<label class="form-check-label" for="vtHistorize">
|
||||
Historize <small class="text-muted">(route evaluations to IHistoryWriter)</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_error is not null)
|
||||
{
|
||||
<section class="panel notice mt-3"><span class="s-bad">@_error</span></section>
|
||||
}
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-primary" disabled="@_busy" @onclick="SaveAsync">Save</button>
|
||||
<button class="btn btn-sm btn-secondary ms-2" @onclick="() => _showForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public long GenerationId { get; set; }
|
||||
[Parameter] public string ClusterId { get; set; } = string.Empty;
|
||||
|
||||
private static readonly string[] DataTypes =
|
||||
["Boolean", "Int32", "Int64", "Float32", "Float64", "String", "DateTime"];
|
||||
|
||||
private bool _loading = true;
|
||||
private bool _busy;
|
||||
private bool _showForm;
|
||||
private List<VirtualTag> _tags = [];
|
||||
private List<Script> _scripts = [];
|
||||
private string? _error;
|
||||
|
||||
// Draft form state (VirtualTag doesn't have update besides Enabled — add-only form)
|
||||
private VirtualTag _draft = NewDraft();
|
||||
private int? _timerMs;
|
||||
|
||||
private static VirtualTag NewDraft() => new()
|
||||
{
|
||||
VirtualTagId = string.Empty,
|
||||
EquipmentId = string.Empty,
|
||||
Name = string.Empty,
|
||||
DataType = "Float32",
|
||||
ScriptId = string.Empty,
|
||||
ChangeTriggered = true,
|
||||
};
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_loading = true;
|
||||
_tags = await VirtualTagSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
_scripts = await ScriptSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private void StartNew()
|
||||
{
|
||||
_draft = NewDraft();
|
||||
_timerMs = null;
|
||||
_error = null;
|
||||
_showForm = true;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_draft.EquipmentId) ||
|
||||
string.IsNullOrWhiteSpace(_draft.Name) ||
|
||||
string.IsNullOrWhiteSpace(_draft.ScriptId))
|
||||
{
|
||||
_error = "Equipment ID, Name, and Script are required.";
|
||||
return;
|
||||
}
|
||||
if (!_draft.ChangeTriggered && _timerMs is null)
|
||||
{
|
||||
_error = "At least one trigger must be set (change-triggered or timer).";
|
||||
return;
|
||||
}
|
||||
|
||||
_busy = true;
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
await VirtualTagSvc.AddAsync(
|
||||
GenerationId,
|
||||
_draft.EquipmentId, _draft.Name, _draft.DataType, _draft.ScriptId,
|
||||
_draft.ChangeTriggered, _timerMs, _draft.Historize,
|
||||
CancellationToken.None);
|
||||
_showForm = false;
|
||||
_tags = await VirtualTagSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex) { _error = ex.Message; }
|
||||
finally { _busy = false; }
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(string id)
|
||||
{
|
||||
await VirtualTagSvc.DeleteAsync(GenerationId, id, CancellationToken.None);
|
||||
_tags = await VirtualTagSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task ToggleEnabledAsync(VirtualTag t)
|
||||
{
|
||||
await VirtualTagSvc.UpdateEnabledAsync(GenerationId, t.VirtualTagId, !t.Enabled, CancellationToken.None);
|
||||
_tags = await VirtualTagSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||
}
|
||||
|
||||
private string ScriptName(string scriptId)
|
||||
{
|
||||
var s = _scripts.FirstOrDefault(x => x.ScriptId == scriptId);
|
||||
return s is not null ? s.Name : scriptId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user