fix(central-ui): Sites page re-check no longer holds the deploy controls; pluralise; clear stale caption on error
The forced refresh after a successful Deploy Artifacts ran inside the try, so the Bulk-actions toggle and every per-site Deploy item stayed disabled for a whole fleet evaluation after the success toast; it now runs after the finally, behind a re-render, with the Re-check spinner showing it in flight. Also: pluralise the instance line's noun, drop a stale "as of" caption when a re-check faults, skip the fleet evaluation when the site load itself failed, and render the badge tooltip through UtcDateTime so the UTC label is honest. Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
This commit is contained in:
@@ -242,6 +242,10 @@
|
|||||||
}
|
}
|
||||||
_loading = false;
|
_loading = false;
|
||||||
|
|
||||||
|
// Nothing to badge: the page is showing an error banner, not a card grid, so a
|
||||||
|
// whole-fleet flatten would buy nothing.
|
||||||
|
if (_errorMessage != null) return;
|
||||||
|
|
||||||
// Render the cards now — the pending-updates evaluation below is a whole-fleet
|
// Render the cards now — the pending-updates evaluation below is a whole-fleet
|
||||||
// flatten and must never hold the list behind it.
|
// flatten and must never hold the list behind it.
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
@@ -268,6 +272,10 @@
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
// The error wins over whatever a previous evaluation said: a failed re-check
|
||||||
|
// must not leave a stale "as of" caption standing next to unavailable badges.
|
||||||
|
_updates = null;
|
||||||
|
_updatesBySite = new Dictionary<int, SitePendingUpdates>();
|
||||||
_updatesError = ex.Message;
|
_updatesError = ex.Message;
|
||||||
Logger.LogWarning(ex, "Pending-updates evaluation failed; Sites page badges show unavailable.");
|
Logger.LogWarning(ex, "Pending-updates evaluation failed; Sites page badges show unavailable.");
|
||||||
}
|
}
|
||||||
@@ -301,6 +309,7 @@
|
|||||||
|
|
||||||
private async Task DeployArtifacts(Site site)
|
private async Task DeployArtifacts(Site site)
|
||||||
{
|
{
|
||||||
|
var succeeded = false;
|
||||||
_deploying = true;
|
_deploying = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -308,12 +317,9 @@
|
|||||||
var result = await ArtifactDeploymentService.RetryForSiteAsync(
|
var result = await ArtifactDeploymentService.RetryForSiteAsync(
|
||||||
site.Id, site.SiteIdentifier, user);
|
site.Id, site.SiteIdentifier, user);
|
||||||
|
|
||||||
if (result.IsSuccess)
|
succeeded = result.IsSuccess;
|
||||||
{
|
if (succeeded)
|
||||||
_toast.ShowSuccess($"Artifacts deployed to '{site.Name}'.");
|
_toast.ShowSuccess($"Artifacts deployed to '{site.Name}'.");
|
||||||
// Force a re-evaluation so the badge flips to current immediately.
|
|
||||||
await LoadUpdatesAsync(force: true);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
_toast.ShowError($"Deploy to '{site.Name}' failed: {result.Error}");
|
_toast.ShowError($"Deploy to '{site.Name}' failed: {result.Error}");
|
||||||
}
|
}
|
||||||
@@ -325,23 +331,32 @@
|
|||||||
{
|
{
|
||||||
_deploying = false;
|
_deploying = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (succeeded)
|
||||||
|
{
|
||||||
|
// Re-enable the deploy controls FIRST: the forced re-evaluation is a whole-fleet
|
||||||
|
// flatten, and the operator should not sit behind it once the deploy has landed.
|
||||||
|
// Its own progress shows on the Re-check button's spinner.
|
||||||
|
StateHasChanged();
|
||||||
|
await LoadUpdatesAsync(force: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeployArtifactsToAllSites()
|
private async Task DeployArtifactsToAllSites()
|
||||||
{
|
{
|
||||||
|
var succeeded = false;
|
||||||
_deploying = true;
|
_deploying = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var user = await GetCurrentUserAsync();
|
var user = await GetCurrentUserAsync();
|
||||||
var result = await ArtifactDeploymentService.DeployToAllSitesAsync(user);
|
var result = await ArtifactDeploymentService.DeployToAllSitesAsync(user);
|
||||||
|
|
||||||
if (result.IsSuccess)
|
succeeded = result.IsSuccess;
|
||||||
|
if (succeeded)
|
||||||
{
|
{
|
||||||
var summary = result.Value!;
|
var summary = result.Value!;
|
||||||
_toast.ShowSuccess(
|
_toast.ShowSuccess(
|
||||||
$"Artifacts deployed: {summary.SuccessCount} succeeded, {summary.FailureCount} failed.");
|
$"Artifacts deployed: {summary.SuccessCount} succeeded, {summary.FailureCount} failed.");
|
||||||
// Force a re-evaluation so the badges flip to current immediately.
|
|
||||||
await LoadUpdatesAsync(force: true);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -356,6 +371,13 @@
|
|||||||
{
|
{
|
||||||
_deploying = false;
|
_deploying = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (succeeded)
|
||||||
|
{
|
||||||
|
// Re-enable the deploy controls FIRST — see DeployArtifacts.
|
||||||
|
StateHasChanged();
|
||||||
|
await LoadUpdatesAsync(force: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-site Artifacts badge. Bootstrap .badge markup, matching the Topology page's
|
// Per-site Artifacts badge. Bootstrap .badge markup, matching the Topology page's
|
||||||
@@ -381,7 +403,7 @@
|
|||||||
break;
|
break;
|
||||||
case ArtifactDeploymentState.OutOfDate:
|
case ArtifactDeploymentState.OutOfDate:
|
||||||
<span class="badge bg-warning text-dark ms-2"
|
<span class="badge bg-warning text-dark ms-2"
|
||||||
title="@(u.ArtifactsDeployedAt is { } at ? $"Artifacts last deployed {at.ToString("u")}" : null)"
|
title="@(u.ArtifactsDeployedAt is { } at ? $"Artifacts last deployed {at.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss")} UTC" : null)"
|
||||||
aria-label="Artifacts: out of date"><i class="bi bi-exclamation-triangle me-1"></i>Artifacts out of date</span>
|
aria-label="Artifacts: out of date"><i class="bi bi-exclamation-triangle me-1"></i>Artifacts out of date</span>
|
||||||
break;
|
break;
|
||||||
case ArtifactDeploymentState.NeverDeployed:
|
case ArtifactDeploymentState.NeverDeployed:
|
||||||
@@ -396,9 +418,11 @@
|
|||||||
// the rendered text stays contiguous (Razor would interleave layout whitespace).
|
// the rendered text stays contiguous (Razor would interleave layout whitespace).
|
||||||
private static string InstanceSummary(SitePendingUpdates u)
|
private static string InstanceSummary(SitePendingUpdates u)
|
||||||
{
|
{
|
||||||
|
var noun = u.DeployedInstanceCount == 1 ? "instance" : "instances";
|
||||||
|
|
||||||
var text = u.StaleInstanceCount == 0
|
var text = u.StaleInstanceCount == 0
|
||||||
? $"All {u.DeployedInstanceCount} instances current"
|
? $"All {u.DeployedInstanceCount} {noun} current"
|
||||||
: $"{u.StaleInstanceCount} of {u.DeployedInstanceCount} instances out of date";
|
: $"{u.StaleInstanceCount} of {u.DeployedInstanceCount} {noun} out of date";
|
||||||
|
|
||||||
if (u.UnknownInstanceCount > 0)
|
if (u.UnknownInstanceCount > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -179,6 +179,27 @@ public class SitesPageTests : BunitContext
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InstanceLine_PluralisesTheSingleInstanceCase()
|
||||||
|
{
|
||||||
|
RegisterServices();
|
||||||
|
_siteRepo.GetAllSitesAsync().Returns(Sites(1, 2));
|
||||||
|
_siteRepo.GetAllDataConnectionsAsync().Returns(new List<DataConnection>());
|
||||||
|
_pendingUpdates.GetAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(_ => Task.FromResult(Snapshot(
|
||||||
|
Pending(1, ArtifactDeploymentState.Current, deployed: 1, stale: 1),
|
||||||
|
Pending(2, ArtifactDeploymentState.Current, deployed: 1))));
|
||||||
|
|
||||||
|
var cut = Render<SitesPage>();
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
{
|
||||||
|
Assert.Contains("1 of 1 instance out of date", cut.Markup);
|
||||||
|
Assert.Contains("All 1 instance current", cut.Markup);
|
||||||
|
Assert.DoesNotContain("1 instances", cut.Markup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Recheck_ForcesRefresh()
|
public void Recheck_ForcesRefresh()
|
||||||
{
|
{
|
||||||
@@ -193,6 +214,46 @@ public class SitesPageTests : BunitContext
|
|||||||
_pendingUpdates.Received().GetAsync(true, Arg.Any<CancellationToken>()));
|
_pendingUpdates.Received().GetAsync(true, Arg.Any<CancellationToken>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SiteLoadFailure_SkipsTheFleetEvaluation()
|
||||||
|
{
|
||||||
|
RegisterServices();
|
||||||
|
_siteRepo.GetAllSitesAsync()
|
||||||
|
.Returns<Task<IReadOnlyList<Site>>>(_ => throw new InvalidOperationException("db down"));
|
||||||
|
|
||||||
|
var cut = Render<SitesPage>();
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() => Assert.Contains("Failed to load sites", cut.Markup));
|
||||||
|
// No cards to badge, so the whole-fleet flatten must not run at all.
|
||||||
|
_pendingUpdates.DidNotReceive().GetAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FailedRecheck_ClearsTheStaleAsOfCaption()
|
||||||
|
{
|
||||||
|
RegisterServices();
|
||||||
|
_siteRepo.GetAllSitesAsync().Returns(Sites(1));
|
||||||
|
_siteRepo.GetAllDataConnectionsAsync().Returns(new List<DataConnection>());
|
||||||
|
_pendingUpdates.GetAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(_ => Task.FromResult(Snapshot(Pending(1, ArtifactDeploymentState.Current))));
|
||||||
|
|
||||||
|
var cut = Render<SitesPage>();
|
||||||
|
cut.WaitForAssertion(() => Assert.Contains("Updates as of", cut.Markup));
|
||||||
|
|
||||||
|
// The re-check now faults: the error wins, so the caption from the earlier good
|
||||||
|
// evaluation must not stand next to "unavailable" badges.
|
||||||
|
_pendingUpdates.GetAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns<Task<PendingUpdatesSnapshot>>(_ => throw new InvalidOperationException("evaluator down"));
|
||||||
|
cut.Find("button[aria-label=\"Re-check pending updates\"]").Click();
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain("Updates as of", cut.Markup);
|
||||||
|
Assert.Contains("Artifacts: unavailable", cut.Markup);
|
||||||
|
Assert.Contains("Site1", cut.Markup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CacheFault_ShowsUnavailable_AndSitesStillRender()
|
public void CacheFault_ShowsUnavailable_AndSitesStillRender()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user