CUB-57: Dashboard summary component with status breakdown
All checks were successful
Dev Build / build-test (pull_request) Successful in 3m13s
All checks were successful
Dev Build / build-test (pull_request) Successful in 3m13s
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
<!-- ============================================================================
|
||||||
|
Dashboard Summary Component Template
|
||||||
|
Displays total active agents, status breakdown, and system health.
|
||||||
|
============================================================================ -->
|
||||||
|
|
||||||
|
<section class="dashboard-summary" role="region" aria-label="Dashboard summary">
|
||||||
|
|
||||||
|
<!-- Total Agents -->
|
||||||
|
<div class="summary-total">
|
||||||
|
<span class="summary-total__count">{{ totalAgents() }}</span>
|
||||||
|
<span class="summary-total__label">Total Agents</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Breakdown -->
|
||||||
|
<div class="summary-breakdown" role="list" aria-label="Agent status breakdown">
|
||||||
|
@for (item of statusItems(); track item.status) {
|
||||||
|
<div class="status-item {{ statusClass(item.status) }}" role="listitem">
|
||||||
|
<span class="status-item__dot" [attr.aria-hidden]="true"></span>
|
||||||
|
<span class="status-item__label">{{ item.label }}</span>
|
||||||
|
<span class="status-item__count">{{ item.count }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Health Indicator -->
|
||||||
|
<div class="health-indicator {{ healthClass() }}" role="status" aria-label="System health: {{ healthLabel() }}">
|
||||||
|
<span class="health-indicator__dot" [attr.aria-hidden]="true"></span>
|
||||||
|
<span class="health-indicator__label">System</span>
|
||||||
|
<span class="health-indicator__value">{{ healthLabel() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Dashboard Summary Component Styles
|
||||||
|
// Uses design tokens from styles/_tokens.scss (CUB-21)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
@use '../../../styles/tokens' as tokens;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Host block
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Summary container
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.dashboard-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: tokens.$spacing-5;
|
||||||
|
padding: tokens.$spacing-6;
|
||||||
|
background-color: tokens.$color-surface-medium;
|
||||||
|
border-radius: tokens.$card-border-radius;
|
||||||
|
border: 1px solid tokens.$color-surface-lighter;
|
||||||
|
box-shadow: tokens.$shadow-level-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Total agents
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.summary-total {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: tokens.$spacing-1;
|
||||||
|
padding-bottom: tokens.$spacing-4;
|
||||||
|
border-bottom: 1px solid tokens.$color-surface-lighter;
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
font-family: tokens.$font-family-brand;
|
||||||
|
font-size: tokens.$font-size-display-small;
|
||||||
|
font-weight: tokens.$font-weight-heavy;
|
||||||
|
line-height: tokens.$line-height-tight;
|
||||||
|
color: tokens.$color-on-surface;
|
||||||
|
letter-spacing: tokens.$letter-spacing-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-family: tokens.$font-family-body;
|
||||||
|
font-size: tokens.$font-size-label-large;
|
||||||
|
font-weight: tokens.$font-weight-medium;
|
||||||
|
color: tokens.$color-on-surface-variant;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: tokens.$letter-spacing-wide;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Status breakdown
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.summary-breakdown {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: tokens.$spacing-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Individual status item
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: tokens.$spacing-2;
|
||||||
|
padding: tokens.$spacing-3 tokens.$spacing-4;
|
||||||
|
border-radius: tokens.$radius-md;
|
||||||
|
background-color: tokens.$color-surface-dark;
|
||||||
|
border: 1px solid tokens.$color-surface-lighter;
|
||||||
|
transition: background-color tokens.$duration-short tokens.$easing-standard;
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
width: tokens.$status-dot-size;
|
||||||
|
height: tokens.$status-dot-size;
|
||||||
|
border-radius: tokens.$radius-full;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
flex: 1;
|
||||||
|
font-family: tokens.$font-family-body;
|
||||||
|
font-size: tokens.$font-size-body-medium;
|
||||||
|
font-weight: tokens.$font-weight-medium;
|
||||||
|
color: tokens.$color-on-surface-variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
font-family: tokens.$font-family-mono;
|
||||||
|
font-size: tokens.$font-size-body-large;
|
||||||
|
font-weight: tokens.$font-weight-bold;
|
||||||
|
color: tokens.$color-on-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status-specific colors using the token map
|
||||||
|
&--active {
|
||||||
|
.status-item__dot {
|
||||||
|
background-color: tokens.$status-active;
|
||||||
|
animation: pulse-active tokens.$duration-standard tokens.$easing-standard infinite;
|
||||||
|
}
|
||||||
|
.status-item__count { color: tokens.$status-active; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--idle {
|
||||||
|
.status-item__dot {
|
||||||
|
background-color: tokens.$status-idle;
|
||||||
|
}
|
||||||
|
.status-item__count { color: tokens.$status-idle; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--thinking {
|
||||||
|
.status-item__dot {
|
||||||
|
background-color: tokens.$status-thinking;
|
||||||
|
animation: pulse-thinking 3s tokens.$easing-standard infinite;
|
||||||
|
}
|
||||||
|
.status-item__count { color: tokens.$status-thinking; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
.status-item__dot {
|
||||||
|
background-color: tokens.$status-error;
|
||||||
|
animation: pulse-error tokens.$duration-fast tokens.$easing-standard infinite;
|
||||||
|
}
|
||||||
|
.status-item__count { color: tokens.$status-error; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// System health indicator
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.health-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: tokens.$spacing-2;
|
||||||
|
padding: tokens.$spacing-3 tokens.$spacing-4;
|
||||||
|
border-radius: tokens.$radius-md;
|
||||||
|
border: 1px solid tokens.$color-surface-lighter;
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
width: tokens.$status-dot-size;
|
||||||
|
height: tokens.$status-dot-size;
|
||||||
|
border-radius: tokens.$radius-full;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-family: tokens.$font-family-body;
|
||||||
|
font-size: tokens.$font-size-body-medium;
|
||||||
|
font-weight: tokens.$font-weight-medium;
|
||||||
|
color: tokens.$color-on-surface-variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
margin-left: auto;
|
||||||
|
font-family: tokens.$font-family-mono;
|
||||||
|
font-size: tokens.$font-size-body-medium;
|
||||||
|
font-weight: tokens.$font-weight-bold;
|
||||||
|
letter-spacing: tokens.$letter-spacing-mono;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--healthy {
|
||||||
|
.health-indicator__dot {
|
||||||
|
background-color: tokens.$status-active;
|
||||||
|
animation: pulse-active tokens.$duration-standard tokens.$easing-standard infinite;
|
||||||
|
}
|
||||||
|
.health-indicator__value { color: tokens.$status-active; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--degraded {
|
||||||
|
.health-indicator__dot {
|
||||||
|
background-color: tokens.$status-error;
|
||||||
|
animation: pulse-error tokens.$duration-fast tokens.$easing-standard infinite;
|
||||||
|
}
|
||||||
|
.health-indicator__value { color: tokens.$status-error; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--unknown {
|
||||||
|
.health-indicator__dot {
|
||||||
|
background-color: tokens.$status-offline;
|
||||||
|
}
|
||||||
|
.health-indicator__value { color: tokens.$status-offline; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pulse animations (from the design system)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@keyframes pulse-active {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.6; transform: scale(0.85); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-thinking {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.5; transform: scale(0.9); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-error {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.7; transform: scale(0.8); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Responsive: stack on compact screens
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@media (max-width: tokens.$breakpoint-compact) {
|
||||||
|
.summary-breakdown {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||||
|
import { AgentStatusService } from '../../services/agent-status.service';
|
||||||
|
import { AgentStatus } from '../../models/agent.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard Summary Component.
|
||||||
|
* Displays total active agents, status breakdown (Active, Idle, Thinking, Error),
|
||||||
|
* and a system health indicator.
|
||||||
|
*
|
||||||
|
* Binds to AgentStatusService.agents signal — no hardcoded values.
|
||||||
|
* Uses design tokens from CUB-21 (styles/_tokens.scss, app/design/tokens.ts).
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard-summary',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './dashboard-summary.component.html',
|
||||||
|
styleUrl: './dashboard-summary.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class DashboardSummaryComponent {
|
||||||
|
private readonly agentStatusService = inject(AgentStatusService);
|
||||||
|
|
||||||
|
/** All agents from the reactive signal */
|
||||||
|
readonly agents = this.agentStatusService.agents;
|
||||||
|
|
||||||
|
/** Total number of agents */
|
||||||
|
readonly totalAgents = computed(() => this.agents().length);
|
||||||
|
|
||||||
|
/** Count of agents per status */
|
||||||
|
readonly statusCounts = computed(() => {
|
||||||
|
const agents = this.agents();
|
||||||
|
const counts: Record<AgentStatus, number> = {
|
||||||
|
active: 0,
|
||||||
|
idle: 0,
|
||||||
|
thinking: 0,
|
||||||
|
error: 0,
|
||||||
|
offline: 0,
|
||||||
|
};
|
||||||
|
for (const agent of agents) {
|
||||||
|
counts[agent.status]++;
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Status items for template iteration */
|
||||||
|
readonly statusItems = computed(() => {
|
||||||
|
const counts = this.statusCounts();
|
||||||
|
const total = this.totalAgents();
|
||||||
|
return [
|
||||||
|
{ status: 'active' as AgentStatus, count: counts.active, label: 'Active' },
|
||||||
|
{ status: 'idle' as AgentStatus, count: counts.idle, label: 'Idle' },
|
||||||
|
{ status: 'thinking' as AgentStatus, count: counts.thinking, label: 'Thinking' },
|
||||||
|
{ status: 'error' as AgentStatus, count: counts.error, label: 'Error' },
|
||||||
|
] as const;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System health indicator:
|
||||||
|
* - 'healthy' → no errors, at least one active agent
|
||||||
|
* - 'degraded' → has errors OR no active agents but some agents exist
|
||||||
|
* - 'unknown' → no agents registered
|
||||||
|
*/
|
||||||
|
readonly systemHealth = computed((): 'healthy' | 'degraded' | 'unknown' => {
|
||||||
|
const agents = this.agents();
|
||||||
|
if (agents.length === 0) return 'unknown';
|
||||||
|
const counts = this.statusCounts();
|
||||||
|
if (counts.error > 0) return 'degraded';
|
||||||
|
if (counts.active > 0) return 'healthy';
|
||||||
|
return 'degraded';
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly healthLabel = computed(() => {
|
||||||
|
const health = this.systemHealth();
|
||||||
|
switch (health) {
|
||||||
|
case 'healthy': return 'Healthy';
|
||||||
|
case 'degraded': return 'Degraded';
|
||||||
|
case 'unknown': return 'Unknown';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Get the CSS class for a given status */
|
||||||
|
statusClass(status: AgentStatus): string {
|
||||||
|
return `status-item--${status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the CSS class for system health */
|
||||||
|
healthClass(): string {
|
||||||
|
return `health-indicator--${this.systemHealth()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/src/app/components/dashboard-summary/index.ts
Normal file
1
frontend/src/app/components/dashboard-summary/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { DashboardSummaryComponent } from './dashboard-summary.component';
|
||||||
@@ -2,4 +2,5 @@ export * from './quick-jump-button/quick-jump-button.component';
|
|||||||
export { AgentStatusBadgeComponent } from './agent-status-badge/agent-status-badge.component';
|
export { AgentStatusBadgeComponent } from './agent-status-badge/agent-status-badge.component';
|
||||||
export { QuickJumpDrawerComponent } from './quick-jump-drawer/index';
|
export { QuickJumpDrawerComponent } from './quick-jump-drawer/index';
|
||||||
export { AgentSessionDrawerComponent } from './agent-session-drawer/index';
|
export { AgentSessionDrawerComponent } from './agent-session-drawer/index';
|
||||||
export type { SessionLogLine, SessionMessage } from './agent-session-drawer/index';
|
export type { SessionLogLine, SessionMessage } from './agent-session-drawer/index';
|
||||||
|
export { DashboardSummaryComponent } from './dashboard-summary/index';
|
||||||
Reference in New Issue
Block a user