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:
@@ -159,7 +159,9 @@ import {
|
||||
cy="16"
|
||||
r="5"
|
||||
[class]="'health-dot health-dot--' + (node.healthStatus ?? 'unknown')"
|
||||
/>
|
||||
>
|
||||
<title>Health: {{ node.healthStatus ?? 'unknown' }}</title>
|
||||
</circle>
|
||||
|
||||
<!-- Environment name -->
|
||||
<text x="26" y="20" class="env-name">{{ truncate(node.label, 18) }}</text>
|
||||
@@ -172,8 +174,8 @@ import {
|
||||
[class]="'env-type env-type--' + (node.environmentType ?? 'development')"
|
||||
>{{ formatEnvType(node.environmentType) }}</text>
|
||||
|
||||
<!-- Bottom row: host count + release -->
|
||||
<text x="14" y="48" class="env-meta">
|
||||
<!-- Middle row: host count + release -->
|
||||
<text x="14" y="42" class="env-meta">
|
||||
{{ node.hostCount }} host{{ node.hostCount !== 1 ? 's' : '' }}
|
||||
· {{ node.targetCount }} target{{ node.targetCount !== 1 ? 's' : '' }}
|
||||
</text>
|
||||
@@ -181,17 +183,54 @@ import {
|
||||
@if (node.currentReleaseId) {
|
||||
<text
|
||||
[attr.x]="node.width - 8"
|
||||
y="48"
|
||||
y="42"
|
||||
text-anchor="end"
|
||||
class="env-release"
|
||||
>{{ truncate(node.currentReleaseId, 12) }}</text>
|
||||
}
|
||||
|
||||
<!-- Deployment status badges (bottom row) -->
|
||||
<g class="deploy-badges" transform="translate(8, 54)">
|
||||
@if (node.deployingCount > 0) {
|
||||
<g class="badge badge--deploying badge--pulse">
|
||||
<rect [attr.x]="0" y="0" [attr.width]="28" height="14" rx="7" />
|
||||
<text x="14" y="10">{{ node.deployingCount }}</text>
|
||||
<title>{{ node.deployingCount }} deploying now</title>
|
||||
</g>
|
||||
}
|
||||
@if (node.pendingCount > 0) {
|
||||
<g class="badge badge--pending badge--pulse" [attr.transform]="'translate(' + (node.deployingCount > 0 ? 32 : 0) + ',0)'">
|
||||
<rect x="0" y="0" [attr.width]="28" height="14" rx="7" />
|
||||
<text x="14" y="10">{{ node.pendingCount }}</text>
|
||||
<title>{{ node.pendingCount }} pending approval</title>
|
||||
</g>
|
||||
}
|
||||
@if (node.failedCount > 0) {
|
||||
<g class="badge badge--failed" [attr.transform]="'translate(' + badgeOffset(node, 2) + ',0)'">
|
||||
<rect x="0" y="0" [attr.width]="28" height="14" rx="7" />
|
||||
<text x="14" y="10">{{ node.failedCount }}</text>
|
||||
<title>{{ node.failedCount }} failed deployment{{ node.failedCount > 1 ? 's' : '' }}</title>
|
||||
</g>
|
||||
}
|
||||
<g class="badge badge--total" [attr.transform]="'translate(' + badgeOffset(node, 3) + ',0)'">
|
||||
<rect x="0" y="0" [attr.width]="28" height="14" rx="7" />
|
||||
<text x="14" y="10">{{ node.totalDeployments }}</text>
|
||||
<title>{{ node.totalDeployments }} total deployment{{ node.totalDeployments !== 1 ? 's' : '' }}</title>
|
||||
</g>
|
||||
@if (node.agentStatus === 'no_agent') {
|
||||
<g class="badge badge--warn" [attr.transform]="'translate(' + badgeOffset(node, 4) + ',0)'">
|
||||
<rect x="0" y="0" [attr.width]="14" height="14" rx="7" />
|
||||
<text x="7" y="10">!</text>
|
||||
<title>No agent assigned</title>
|
||||
</g>
|
||||
}
|
||||
</g>
|
||||
|
||||
<!-- Frozen indicator -->
|
||||
@if (node.isFrozen) {
|
||||
<text
|
||||
[attr.x]="node.width - 8"
|
||||
y="64"
|
||||
y="66"
|
||||
text-anchor="end"
|
||||
class="env-frozen"
|
||||
>FROZEN</text>
|
||||
@@ -480,6 +519,50 @@ import {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Deployment status badges */
|
||||
.deploy-badges { pointer-events: all; }
|
||||
|
||||
.badge rect {
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.badge text {
|
||||
font-size: 8px;
|
||||
font-weight: 600;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.badge--deploying rect { fill: var(--color-status-info-bg, #dbeafe); }
|
||||
.badge--deploying text { fill: var(--color-status-info-text, #1d4ed8); }
|
||||
|
||||
.badge--pending rect { fill: var(--color-status-warning-bg); }
|
||||
.badge--pending text { fill: var(--color-status-warning-text); }
|
||||
|
||||
.badge--failed rect { fill: var(--color-status-error-bg); }
|
||||
.badge--failed text { fill: var(--color-status-error-text); }
|
||||
|
||||
.badge--total rect { fill: var(--color-surface-tertiary, #e2e8f0); }
|
||||
.badge--total text { fill: var(--color-text-muted); }
|
||||
|
||||
.badge--warn rect { fill: var(--color-status-warning-bg); }
|
||||
.badge--warn text { fill: var(--color-status-warning-text); font-size: 9px; }
|
||||
|
||||
.badge--pulse {
|
||||
animation: badge-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes badge-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.badge--pulse {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Minimap */
|
||||
.minimap {
|
||||
position: absolute;
|
||||
@@ -607,6 +690,15 @@ export class TopologyGraphComponent {
|
||||
return label.length * 4.5;
|
||||
}
|
||||
|
||||
badgeOffset(node: TopologyPositionedNode, position: number): number {
|
||||
let offset = 0;
|
||||
if (position > 0 && node.deployingCount > 0) offset += 32;
|
||||
if (position > 1 && node.pendingCount > 0) offset += 32;
|
||||
if (position > 2 && node.failedCount > 0) offset += 32;
|
||||
if (position > 3) offset += 32;
|
||||
return offset;
|
||||
}
|
||||
|
||||
truncateEdgeLabel(label: string | undefined | null): string {
|
||||
if (!label) return '';
|
||||
return label.length > 30 ? label.substring(0, 29) + '\u2026' : label;
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface TopologyPositionedNode {
|
||||
currentReleaseId?: string;
|
||||
isFrozen: boolean;
|
||||
promotionPathCount: number;
|
||||
deployingCount: number;
|
||||
pendingCount: number;
|
||||
failedCount: number;
|
||||
totalDeployments: number;
|
||||
agentStatus?: string;
|
||||
}
|
||||
|
||||
export interface TopologyRoutedEdge {
|
||||
|
||||
Reference in New Issue
Block a user