CUB-20: Develop agent card component with dynamic status/progress
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m4s
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m4s
This commit is contained in:
@@ -4,25 +4,118 @@
|
||||
// Mobile (<1024px): single-column stack
|
||||
// ============================================================================
|
||||
|
||||
@use 'tokens' as tokens;
|
||||
|
||||
.hub-page {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
padding: var(--cc-section-padding, 16px);
|
||||
gap: tokens.$cc-card-gap;
|
||||
padding: tokens.$cc-section-padding;
|
||||
min-height: 400px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.hub-page__placeholder {
|
||||
color: var(--cc-on-surface-variant);
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
// Desktop / kiosk breakpoint — 2-column grid
|
||||
@media (min-width: 1024px) {
|
||||
@media (min-width: tokens.$cc-breakpoint-desktop) {
|
||||
.hub-page {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Loading state ──
|
||||
.hub-page__loading {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 48px 24px;
|
||||
color: var(--cc-on-surface-variant);
|
||||
}
|
||||
|
||||
.hub-page__loading-icon {
|
||||
font-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--status-active);
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.hub-page__loading-text {
|
||||
font-size: 16px;
|
||||
color: var(--cc-on-surface-variant);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// ── Empty state ──
|
||||
.hub-page__empty {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hub-page__empty-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--status-offline);
|
||||
}
|
||||
|
||||
.hub-page__empty-text {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--cc-on-surface);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hub-page__empty-hint {
|
||||
font-size: 14px;
|
||||
color: var(--cc-on-surface-variant);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// ── Error state ──
|
||||
.hub-page__error {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--status-error);
|
||||
border-radius: tokens.$cc-card-border-radius;
|
||||
background-color: var(--status-error-bg);
|
||||
}
|
||||
|
||||
.hub-page__error-icon {
|
||||
font-size: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--status-error);
|
||||
}
|
||||
|
||||
.hub-page__error-text {
|
||||
font-size: 14px;
|
||||
color: var(--status-error);
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// ── Accessibility: reduced motion ──
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hub-page__loading-icon {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,154 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Signal,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { AgentCardComponent } from '../../command-hub/components/agent-card/agent-card.component';
|
||||
import { AgentCardData } from '../../models/agent.model';
|
||||
import { AgentStatusService } from '../../services/agent-status.service';
|
||||
|
||||
// ============================================================================
|
||||
// Hub Page — Fleet Status Grid
|
||||
// Per spec Section 7.3: Renders AgentCard components in a responsive grid.
|
||||
// Handles loading, empty, error, and success states.
|
||||
// Uses Angular signals from AgentStatusService for reactive updates.
|
||||
// ============================================================================
|
||||
|
||||
@Component({
|
||||
selector: 'app-hub-page',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
imports: [CommonModule, MatIconModule, AgentCardComponent],
|
||||
template: `
|
||||
<div class="hub-page">
|
||||
<p class="hub-page__placeholder">Command Hub — Fleet status grid will render here</p>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div class="hub-page__loading" *ngIf="loading()">
|
||||
<mat-icon class="hub-page__loading-icon" aria-hidden="true">hourglass_empty</mat-icon>
|
||||
<p class="hub-page__loading-text">Loading agents…</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="hub-page__empty" *ngIf="!loading() && agents().length === 0">
|
||||
<mat-icon class="hub-page__empty-icon" aria-hidden="true">smart_toy</mat-icon>
|
||||
<p class="hub-page__empty-text">No agents connected yet.</p>
|
||||
<p class="hub-page__empty-hint">Agents will appear here once they come online.</p>
|
||||
</div>
|
||||
|
||||
<!-- Agent card grid -->
|
||||
<app-agent-card
|
||||
*ngFor="let agent of agents(); trackBy: trackByAgentId"
|
||||
[status]="agent.status"
|
||||
[task]="agent.currentTask ?? ''"
|
||||
[progress]="agent.taskProgress ?? 0"
|
||||
[sessionKey]="agent.sessionKey"
|
||||
[channel]="agent.channel"
|
||||
[lastActivity]="agent.lastActivity"
|
||||
[agentId]="agent.id"
|
||||
[displayName]="agent.displayName"
|
||||
[role]="agent.role"
|
||||
[errorMessage]="agent.errorMessage ?? ''"
|
||||
[taskElapsed]="agent.taskElapsed ?? ''"
|
||||
/>
|
||||
|
||||
<!-- Error state -->
|
||||
<div class="hub-page__error" *ngIf="error()" [attr.aria-label]="'Error: ' + error()">
|
||||
<mat-icon class="hub-page__error-icon" aria-hidden="true">error_outline</mat-icon>
|
||||
<p class="hub-page__error-text">{{ error() }}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './hub-page.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class HubPageComponent {}
|
||||
export class HubPageComponent implements OnInit {
|
||||
/** Agent list signal from the status service */
|
||||
readonly agents: Signal<AgentCardData[]>;
|
||||
|
||||
/** Whether the initial load is in progress */
|
||||
readonly loading = computed(() => this._initialLoad);
|
||||
|
||||
/** Error message signal */
|
||||
readonly error = computed(() => this._errorMsg);
|
||||
|
||||
private _initialLoad = true;
|
||||
private _errorMsg: string | null = null;
|
||||
|
||||
private readonly _agentService = inject(AgentStatusService);
|
||||
|
||||
constructor() {
|
||||
this.agents = this._agentService.agents;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Seed mock data for development until SignalR is wired up
|
||||
this._seedMockData();
|
||||
}
|
||||
|
||||
/** TrackBy function for *ngFor performance */
|
||||
trackByAgentId(index: number, agent: AgentCardData): string {
|
||||
return agent.id;
|
||||
}
|
||||
|
||||
// --- Mock data seeding (remove when SignalR is integrated) ---
|
||||
private _seedMockData(): void {
|
||||
const now = new Date();
|
||||
const mockAgents: AgentCardData[] = [
|
||||
{
|
||||
id: 'otto',
|
||||
displayName: 'Otto',
|
||||
role: 'Orchestrator Agent',
|
||||
status: 'active',
|
||||
currentTask: 'Reviewing PR #42',
|
||||
taskProgress: 72,
|
||||
taskElapsed: '04m 12s',
|
||||
sessionKey: 'agent:otto:slack:control-center:8787a',
|
||||
channel: 'slack',
|
||||
lastActivity: new Date(now.getTime() - 30_000),
|
||||
},
|
||||
{
|
||||
id: 'rex',
|
||||
displayName: 'Rex',
|
||||
role: 'Frontend Agent',
|
||||
status: 'thinking',
|
||||
currentTask: 'Building agent card component',
|
||||
taskProgress: 45,
|
||||
taskElapsed: '02m 38s',
|
||||
sessionKey: 'agent:rex:telegram:direct:b3c91',
|
||||
channel: 'telegram',
|
||||
lastActivity: new Date(now.getTime() - 5_000),
|
||||
},
|
||||
{
|
||||
id: 'dex',
|
||||
displayName: 'Dex',
|
||||
role: 'Backend Agent',
|
||||
status: 'idle',
|
||||
currentTask: '',
|
||||
taskProgress: 0,
|
||||
sessionKey: 'agent:dex:slack:control-center:4f2a0',
|
||||
channel: 'slack',
|
||||
lastActivity: new Date(now.getTime() - 300_000),
|
||||
},
|
||||
{
|
||||
id: 'hex',
|
||||
displayName: 'Hex',
|
||||
role: 'Database Agent',
|
||||
status: 'error',
|
||||
currentTask: 'Migration failed',
|
||||
taskProgress: 0,
|
||||
errorMessage: 'Connection timeout to PostgreSQL',
|
||||
sessionKey: 'agent:hex:telegram:direct:9d1e7',
|
||||
channel: 'telegram',
|
||||
lastActivity: new Date(now.getTime() - 1_200_000),
|
||||
},
|
||||
];
|
||||
this._agentService['_agents'].set(mockAgents);
|
||||
this._initialLoad = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user