Files
scadalink-design/src/ScadaLink.CentralUI/Components/Pages/Monitoring/EventLogs.razor
Joseph Doherty 7740a3bcf9 feat: add JoeAppEngine OPC UA nodes, fix DCL auto-reconnect and quality push
- Add JoeAppEngine folder to OPC UA nodes.json (BTCS, AlarmCntsBySeverity, Scheduler/ScanTime)
- Fix DataConnectionActor: capture Self in PreStart for use from non-actor threads,
  preventing Self.Tell failure in Disconnected event handler
- Implement InstanceActor.HandleConnectionQualityChanged to mark attributes Bad on disconnect
- Fix LmxFakeProxy TagMapper to serialize arrays as JSON instead of "System.Int32[]"
- Allow DataType and DataSourceReference updates in TemplateService.UpdateAttributeAsync
- Update test_infra_opcua.md with JoeAppEngine documentation
2026-03-19 13:27:54 -04:00

189 lines
7.1 KiB
Plaintext

@page "/monitoring/event-logs"
@attribute [Authorize]
@using ScadaLink.Commons.Entities.Sites
@using ScadaLink.Commons.Interfaces.Repositories
@using ScadaLink.Commons.Messages.RemoteQuery
@using ScadaLink.Communication
@inject ISiteRepository SiteRepository
@inject CommunicationService CommunicationService
<div class="container-fluid mt-3">
<h4 class="mb-3">Site Event Logs</h4>
<ToastNotification @ref="_toast" />
<div class="row mb-3 g-2">
<div class="col-md-2">
<label class="form-label small">Site</label>
<select class="form-select form-select-sm" @bind="_selectedSiteId">
<option value="">Select site...</option>
@foreach (var site in _sites)
{
<option value="@site.SiteIdentifier">@site.Name</option>
}
</select>
</div>
<div class="col-md-2">
<label class="form-label small">Event Type</label>
<input type="text" class="form-control form-control-sm" @bind="_filterEventType" placeholder="e.g. ScriptError" />
</div>
<div class="col-md-1">
<label class="form-label small">Severity</label>
<select class="form-select form-select-sm" @bind="_filterSeverity">
<option value="">All</option>
<option>Info</option>
<option>Warning</option>
<option>Error</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small">From</label>
<input type="datetime-local" class="form-control form-control-sm" @bind="_filterFrom" />
</div>
<div class="col-md-2">
<label class="form-label small">To</label>
<input type="datetime-local" class="form-control form-control-sm" @bind="_filterTo" />
</div>
<div class="col-md-1">
<label class="form-label small">Keyword</label>
<input type="text" class="form-control form-control-sm" @bind="_filterKeyword" />
</div>
<div class="col-md-2">
<label class="form-label small">Instance</label>
<input type="text" class="form-control form-control-sm" @bind="_filterInstanceName" placeholder="Instance name" />
</div>
<div class="col-md-1 d-flex align-items-end">
<button class="btn btn-primary btn-sm" @onclick="Search" disabled="@(string.IsNullOrEmpty(_selectedSiteId) || _searching)">
@if (_searching) { <span class="spinner-border spinner-border-sm"></span> }
Search
</button>
</div>
</div>
@if (_errorMessage != null)
{
<div class="alert alert-danger">@_errorMessage</div>
}
@if (_entries != null)
{
<table class="table table-sm table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Timestamp</th>
<th>Type</th>
<th>Severity</th>
<th>Instance</th>
<th>Source</th>
<th>Message</th>
</tr>
</thead>
<tbody>
@if (_entries.Count == 0)
{
<tr><td colspan="6" class="text-muted text-center">No events found.</td></tr>
}
@foreach (var entry in _entries)
{
<tr class="@(entry.Severity == "Error" ? "table-danger" : entry.Severity == "Warning" ? "table-warning" : "")">
<td class="small"><TimestampDisplay Value="@entry.Timestamp" /></td>
<td class="small">@entry.EventType</td>
<td><span class="badge @GetSeverityBadge(entry.Severity)">@entry.Severity</span></td>
<td class="small">@(entry.InstanceId ?? "—")</td>
<td class="small">@entry.Source</td>
<td class="small">@entry.Message</td>
</tr>
}
</tbody>
</table>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">@_entries.Count entries loaded</span>
@if (_hasMore)
{
<button class="btn btn-outline-primary btn-sm" @onclick="LoadMore" disabled="@_searching">Load More</button>
}
</div>
}
</div>
@code {
private List<Site> _sites = new();
private string _selectedSiteId = string.Empty;
private string? _filterEventType;
private string _filterSeverity = string.Empty;
private DateTime? _filterFrom;
private DateTime? _filterTo;
private string? _filterKeyword;
private string? _filterInstanceName;
private List<EventLogEntry>? _entries;
private bool _hasMore;
private long? _continuationToken;
private bool _searching;
private string? _errorMessage;
private ToastNotification _toast = default!;
protected override async Task OnInitializedAsync()
{
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
}
private async Task Search()
{
_entries = new();
_continuationToken = null;
await FetchPage();
}
private async Task LoadMore() => await FetchPage();
private async Task FetchPage()
{
_searching = true;
_errorMessage = null;
try
{
var request = new EventLogQueryRequest(
CorrelationId: Guid.NewGuid().ToString("N"),
SiteId: _selectedSiteId,
From: _filterFrom.HasValue ? new DateTimeOffset(_filterFrom.Value, TimeSpan.Zero) : null,
To: _filterTo.HasValue ? new DateTimeOffset(_filterTo.Value, TimeSpan.Zero) : null,
EventType: string.IsNullOrWhiteSpace(_filterEventType) ? null : _filterEventType.Trim(),
Severity: string.IsNullOrWhiteSpace(_filterSeverity) ? null : _filterSeverity,
InstanceId: string.IsNullOrWhiteSpace(_filterInstanceName) ? null : _filterInstanceName.Trim(),
KeywordFilter: string.IsNullOrWhiteSpace(_filterKeyword) ? null : _filterKeyword.Trim(),
ContinuationToken: _continuationToken,
PageSize: 50,
Timestamp: DateTimeOffset.UtcNow);
var response = await CommunicationService.QueryEventLogsAsync(_selectedSiteId, request);
if (response.Success)
{
_entries ??= new();
_entries.AddRange(response.Entries);
_hasMore = response.HasMore;
_continuationToken = response.ContinuationToken;
}
else
{
_errorMessage = response.ErrorMessage ?? "Query failed.";
}
}
catch (Exception ex)
{
_errorMessage = $"Query failed: {ex.Message}";
}
_searching = false;
}
private static string GetSeverityBadge(string severity) => severity switch
{
"Error" => "bg-danger",
"Warning" => "bg-warning text-dark",
"Info" => "bg-info text-dark",
_ => "bg-secondary"
};
}