feat(kpi): K16 — Health dashboard per-site trend panel
This commit is contained in:
@@ -14,6 +14,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
using HealthPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.Health;
|
||||
@@ -65,6 +66,17 @@ public class HealthPageTests : BunitContext
|
||||
.Returns(new Dictionary<string, SiteHealthState>());
|
||||
Services.AddSingleton(aggregator);
|
||||
|
||||
// M6 K16 — the Health page now injects IKpiHistoryQueryService to feed the
|
||||
// per-site Site Health Trends panel. Stub it with a known non-empty series
|
||||
// so the page resolves the dependency and the trend charts have data; the
|
||||
// dedicated trend tests below seed sites / override behaviour.
|
||||
var kpiHistory = Substitute.For<IKpiHistoryQueryService>();
|
||||
kpiHistory.GetSeriesAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
|
||||
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<KpiSeriesPoint>>(SampleSeries()));
|
||||
Services.AddSingleton(kpiHistory);
|
||||
|
||||
var siteRepo = Substitute.For<ISiteRepository>();
|
||||
siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<Site>>(new List<Site>()));
|
||||
@@ -210,6 +222,89 @@ public class HealthPageTests : BunitContext
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Renders_SiteHealthTrends_PanelAndChart_ForSelectedSite()
|
||||
{
|
||||
// Seed one site so the trend panel's selector has an option and the
|
||||
// default-site load produces charts.
|
||||
SeedSites("site-a");
|
||||
|
||||
var cut = Render<HealthPage>();
|
||||
|
||||
cut.WaitForAssertion(() =>
|
||||
{
|
||||
// The panel + its site selector render at the documented hooks.
|
||||
Assert.Contains("data-test=\"site-health-trends\"", cut.Markup);
|
||||
Assert.Contains("data-test=\"site-health-trends-site\"", cut.Markup);
|
||||
// The four metric charts render (the shared KpiTrendChart slug hook),
|
||||
// and the seeded non-empty series draws a polyline.
|
||||
Assert.Contains("kpi-trend-connections-down", cut.Markup);
|
||||
Assert.Contains("kpi-trend-dead-letters", cut.Markup);
|
||||
Assert.Contains("kpi-trend-script-errors", cut.Markup);
|
||||
Assert.Contains("<polyline", cut.Markup);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SiteHealthTrendsFailure_DoesNotBreakDashboard()
|
||||
{
|
||||
SeedSites("site-a");
|
||||
|
||||
// The KPI-history service throws on every query — the trend load is
|
||||
// best-effort, so the dashboard (and its tiles) must still render.
|
||||
var faulting = Substitute.For<IKpiHistoryQueryService>();
|
||||
faulting.GetSeriesAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
|
||||
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task<IReadOnlyList<KpiSeriesPoint>>>(_ =>
|
||||
throw new InvalidOperationException("kpi history unavailable"));
|
||||
Services.AddSingleton(faulting);
|
||||
|
||||
var cut = Render<HealthPage>();
|
||||
|
||||
cut.WaitForAssertion(() =>
|
||||
{
|
||||
// No unhandled exception: the core dashboard tiles still render, and
|
||||
// the panel falls back to the per-chart unavailable placeholder.
|
||||
Assert.Contains("Notification Outbox", cut.Markup);
|
||||
Assert.Contains("data-test=\"site-health-trends\"", cut.Markup);
|
||||
Assert.Contains("Trend data unavailable.", cut.Markup);
|
||||
});
|
||||
}
|
||||
|
||||
// Re-seeds the aggregator substitute so the trend panel's site selector has
|
||||
// options. Each site id maps to a minimal online SiteHealthState (a null
|
||||
// report is fine — the trend panel keys off the site ids, not the report).
|
||||
private void SeedSites(params string[] siteIds)
|
||||
{
|
||||
var aggregator = Substitute.For<ICentralHealthAggregator>();
|
||||
var states = siteIds.ToDictionary(
|
||||
id => id,
|
||||
id => new SiteHealthState
|
||||
{
|
||||
SiteId = id,
|
||||
IsOnline = true,
|
||||
LastHeartbeatAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
aggregator.GetAllSiteStates()
|
||||
.Returns(new Dictionary<string, SiteHealthState>(states));
|
||||
Services.AddSingleton(aggregator);
|
||||
}
|
||||
|
||||
// A known non-empty (≥2-point) series so KpiTrendChart renders a polyline
|
||||
// rather than the single-sample / unavailable placeholder.
|
||||
private static IReadOnlyList<KpiSeriesPoint> SampleSeries()
|
||||
{
|
||||
var baseUtc = DateTime.UtcNow.AddHours(-24);
|
||||
return new List<KpiSeriesPoint>
|
||||
{
|
||||
new(baseUtc, 1),
|
||||
new(baseUtc.AddHours(6), 3),
|
||||
new(baseUtc.AddHours(12), 2),
|
||||
new(baseUtc.AddHours(18), 5),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
|
||||
Reference in New Issue
Block a user