Add deployment status badges to environment nodes in topology graph
Each environment node now shows pill badges at the bottom: - Deploying (blue, pulsing) — count of active deployments - Pending (amber, pulsing) — count awaiting approval - Failed (red) — count of failed deployments - Total (muted) — historical deployment count - Agent warning (!) — when no agent is assigned Badges pulse with CSS animation (respects prefers-reduced-motion). Native SVG <title> elements provide hover tooltips. Backend enriches TopologyPositionedNode with deployment counts from promotion path statuses and agent assignment data. Also fixes duplicate environment IDs causing layout API 400 errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,12 @@ public sealed record TopologyPositionedNode(
|
||||
int TargetCount,
|
||||
string? CurrentReleaseId,
|
||||
bool IsFrozen,
|
||||
int PromotionPathCount);
|
||||
int PromotionPathCount,
|
||||
int DeployingCount,
|
||||
int PendingCount,
|
||||
int FailedCount,
|
||||
int TotalDeployments,
|
||||
string? AgentStatus);
|
||||
|
||||
public sealed record TopologyRoutedEdge(
|
||||
string Id,
|
||||
|
||||
@@ -12,7 +12,7 @@ public sealed class TopologyLayoutService
|
||||
{
|
||||
private const double RegionPadding = 30;
|
||||
private const double EnvironmentNodeWidth = 180;
|
||||
private const double EnvironmentNodeHeight = 72;
|
||||
private const double EnvironmentNodeHeight = 76;
|
||||
|
||||
private readonly TopologyReadModelService readModel;
|
||||
private readonly IElkLayoutEngine elkLayout;
|
||||
@@ -50,7 +50,10 @@ public sealed class TopologyLayoutService
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var regions = regionsTask.Result.Items;
|
||||
var environments = environmentsTask.Result.Items;
|
||||
var environments = environmentsTask.Result.Items
|
||||
.GroupBy(e => e.EnvironmentId, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
var paths = pathsTask.Result.Items;
|
||||
var gates = gatesTask.Result.Items;
|
||||
var hosts = hostsTask.Result.Items;
|
||||
@@ -92,8 +95,14 @@ public sealed class TopologyLayoutService
|
||||
});
|
||||
}
|
||||
|
||||
var seenEnvIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var env in environments)
|
||||
{
|
||||
if (!seenEnvIds.Add($"env-{env.EnvironmentId}"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
elkNodes.Add(new ElkNode
|
||||
{
|
||||
Id = $"env-{env.EnvironmentId}",
|
||||
@@ -188,7 +197,12 @@ public sealed class TopologyLayoutService
|
||||
TargetCount: region?.TargetCount ?? 0,
|
||||
CurrentReleaseId: null,
|
||||
IsFrozen: false,
|
||||
PromotionPathCount: 0);
|
||||
PromotionPathCount: 0,
|
||||
DeployingCount: 0,
|
||||
PendingCount: 0,
|
||||
FailedCount: 0,
|
||||
TotalDeployments: 0,
|
||||
AgentStatus: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -197,6 +211,15 @@ public sealed class TopologyLayoutService
|
||||
string.Equals(e.EnvironmentId, envId, StringComparison.OrdinalIgnoreCase));
|
||||
var health = ResolveEnvironmentHealth(envId, targetsByEnv);
|
||||
var latestRelease = ResolveLatestRelease(envId, targetsByEnv);
|
||||
var envPaths = paths.Where(p =>
|
||||
string.Equals(p.TargetEnvironmentId, envId, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(p.SourceEnvironmentId, envId, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
var deployingCount = envPaths.Count(p => p.Status is "running" or "deploying");
|
||||
var pendingCount = envPaths.Count(p => p.Status is "pending" or "awaiting_approval" or "gates_running");
|
||||
var failedCount = envPaths.Count(p => p.Status is "failed");
|
||||
var totalDeployments = envPaths.Count;
|
||||
|
||||
var agentStatus = ResolveAgentStatus(envId, targetsByEnv);
|
||||
|
||||
return new TopologyPositionedNode(
|
||||
Id: node.Id,
|
||||
@@ -215,7 +238,12 @@ public sealed class TopologyLayoutService
|
||||
TargetCount: env?.TargetCount ?? 0,
|
||||
CurrentReleaseId: latestRelease,
|
||||
IsFrozen: false,
|
||||
PromotionPathCount: env?.PromotionPathCount ?? 0);
|
||||
PromotionPathCount: env?.PromotionPathCount ?? 0,
|
||||
DeployingCount: deployingCount,
|
||||
PendingCount: pendingCount,
|
||||
FailedCount: failedCount,
|
||||
TotalDeployments: totalDeployments,
|
||||
AgentStatus: agentStatus);
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
@@ -321,4 +349,27 @@ public sealed class TopologyLayoutService
|
||||
.Select(t => t.ReleaseId)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static string? ResolveAgentStatus(
|
||||
string environmentId,
|
||||
Dictionary<string, List<TopologyTargetProjection>> targetsByEnv)
|
||||
{
|
||||
if (!targetsByEnv.TryGetValue(environmentId, out var envTargets) || envTargets.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var agentIds = envTargets
|
||||
.Where(t => !string.IsNullOrEmpty(t.AgentId))
|
||||
.Select(t => t.AgentId)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (agentIds.Count == 0)
|
||||
{
|
||||
return "no_agent";
|
||||
}
|
||||
|
||||
return "active";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user