Custom Angular Component Metrics Dashboard
This challenge involves building a reusable Angular component that can display custom metrics from various data sources. This is a common requirement for applications that need to visualize performance indicators, user activity, or any quantifiable data in a flexible and standardized way.
Problem Description
You are tasked with creating an AbstractMetricComponent in Angular. This abstract component will serve as a base class for all specific metric components (e.g., UserCountMetricComponent, ApiLatencyMetricComponent). The goal is to establish a common interface for how metrics are fetched, displayed, and managed, allowing for easy integration of new metrics without modifying the dashboard layout component.
Key Requirements:
- Abstract Base Component: Create an abstract class
AbstractMetricComponentthat defines common properties and methods for all metric components. - Data Fetching: The base component should provide a mechanism for subclasses to define how their specific metric data is fetched. This could be an abstract method or a protected method that subclasses must implement.
- Display Logic: The base component should handle common display logic, such as loading states, error handling, and rendering the actual metric value.
- Input Properties: Define input properties for the base component that are common to all metrics (e.g., a title, a unique identifier).
- Specific Metric Component: Implement at least one concrete metric component (e.g.,
UserCountMetricComponent) that extendsAbstractMetricComponentand provides its own data fetching and display logic. - Dashboard Integration: Create a simple
DashboardComponentthat can host multiple instances of differentAbstractMetricComponentsubclasses.
Expected Behavior:
- When the
DashboardComponentloads, eachAbstractMetricComponentinstance should independently fetch its data. - During data fetching, a loading indicator should be visible.
- If data fetching is successful, the metric value should be displayed.
- If data fetching fails, an error message should be displayed.
- Each metric component should render with its specified title.
Edge Cases:
- Handling empty data responses.
- Graceful handling of network errors during data fetching.
- Ensuring each metric's loading and error states are independent.
Examples
Example 1: UserCountMetricComponent
// Assuming this is within your Angular project structure
// app/metrics/abstract-metric.component.ts
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { Subject, Observable, of, timer } from 'rxjs';
import { catchError, finalize, takeUntil, tap, switchMap } from 'rxjs/operators';
@Component({ template: '' }) // No template for abstract component
export abstract class AbstractMetricComponent implements OnInit, OnDestroy {
@Input() title: string = 'Untitled Metric';
@Input() metricId: string = ''; // Unique identifier for the metric
loading: boolean = false;
error: string | null = null;
metricValue: any = null;
protected destroy$ = new Subject<void>();
ngOnInit(): void {
this.fetchMetricData();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
protected abstract fetchData(): Observable<any>;
protected fetchMetricData(): void {
this.loading = true;
this.error = null;
this.metricValue = null;
this.fetchData()
.pipe(
tap((data: any) => {
// Basic handling of data - subclasses might need more complex parsing
if (data !== undefined && data !== null) {
this.metricValue = data;
} else {
this.metricValue = 'N/A'; // Or handle as an error
}
}),
catchError((err: any) => {
console.error(`Error fetching metric ${this.metricId}:`, err);
this.error = 'Failed to load metric data.';
return of(null); // Continue observable stream
}),
finalize(() => {
this.loading = false;
}),
takeUntil(this.destroy$)
)
.subscribe();
}
}
// app/metrics/user-count-metric/user-count-metric.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { AbstractMetricComponent } from '../abstract-metric.component';
import { Observable, of, timer } from 'rxjs';
import { map, delay } from 'rxjs/operators';
@Component({
selector: 'app-user-count-metric',
template: `
<div class="metric-card">
<h3>{{ title }}</h3>
<div class="metric-content">
<div *ngIf="loading">Loading...</div>
<div *ngIf="error">{{ error }}</div>
<div *ngIf="!loading && !error && metricValue !== null">
<strong>{{ metricValue }}</strong> active users
</div>
</div>
</div>
`,
styleUrls: ['./user-count-metric.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCountMetricComponent extends AbstractMetricComponent {
protected fetchData(): Observable<number> {
// Simulate fetching user count data from an API
// In a real app, this would be an HttpClient call
const simulatedUserCount = Math.floor(Math.random() * 1000);
return of(simulatedUserCount).pipe(
delay(1500), // Simulate network latency
map(count => count) // Ensure return type is Observable<number>
);
}
}
// app/dashboard/dashboard.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
template: `
<h1>Dashboard</h1>
<div class="dashboard-grid">
<app-user-count-metric
title="Current Users Online"
metricId="users-online"
></app-user-count-metric>
<!-- Potentially other metric components here -->
<!-- <app-api-latency-metric title="API Latency" metricId="api-latency"></app-api-latency-metric> -->
</div>
`,
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent {}
Input to DashboardComponent:
<app-dashboard></app-dashboard>
Expected Output (rendered HTML structure - varies due to random data):
After ~1.5 seconds, the DashboardComponent would display:
<h1>Dashboard</h1>
<div class="dashboard-grid">
<div class="metric-card">
<h3>Current Users Online</h3>
<div class="metric-content">
<strong>456</strong> active users
</div>
</div>
</div>
If an error occurred during fetching for another hypothetical metric:
<h1>Dashboard</h1>
<div class="dashboard-grid">
<div class="metric-card">
<h3>Current Users Online</h3>
<div class="metric-content">
<strong>123</strong> active users
</div>
</div>
<div class="metric-card">
<h3>API Latency</h3>
<div class="metric-content">
Failed to load metric data.
</div>
</div>
</div>
Explanation:
The DashboardComponent renders an instance of UserCountMetricComponent with specific title and metricId inputs. The UserCountMetricComponent extends AbstractMetricComponent, overriding the fetchData method to simulate retrieving a user count. The base AbstractMetricComponent handles the overall loading, error display, and rendering of the value provided by fetchData.
Example 2: Hypothetical ApiLatencyMetricComponent
Assume you have another component:
// app/metrics/api-latency-metric/api-latency-metric.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { AbstractMetricComponent } from '../abstract-metric.component';
import { Observable, of, timer, throwError } from 'rxjs';
import { map, delay } from 'rxjs/operators';
@Component({
selector: 'app-api-latency-metric',
template: `
<div class="metric-card">
<h3>{{ title }}</h3>
<div class="metric-content">
<div *ngIf="loading">Loading...</div>
<div *ngIf="error">{{ error }}</div>
<div *ngIf="!loading && !error && metricValue !== null">
Average: <strong>{{ metricValue }} ms</strong>
</div>
</div>
</div>
`,
styleUrls: ['./api-latency-metric.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiLatencyMetricComponent extends AbstractMetricComponent {
protected fetchData(): Observable<number> {
// Simulate fetching API latency data
const shouldFail = Math.random() > 0.8; // 20% chance of failure
if (shouldFail) {
return throwError(() => new Error('Network timeout')).pipe(delay(1000));
}
const simulatedLatency = Math.floor(Math.random() * 200) + 50; // Latency between 50ms and 250ms
return of(simulatedLatency).pipe(
delay(1000), // Simulate network latency
map(latency => latency)
);
}
}
Input to DashboardComponent:
<app-dashboard>
<div class="dashboard-grid">
<app-user-count-metric title="Active Users" metricId="users"></app-user-count-metric>
<app-api-latency-metric title="Service Latency" metricId="service-latency"></app-api-latency-metric>
</div>
</app-dashboard>
Expected Output:
The dashboard would show both components, with each independently showing "Loading..." for 1-1.5 seconds before displaying their respective data or an error message.
Constraints
- The
AbstractMetricComponentmust be an abstract class. - All concrete metric components must extend
AbstractMetricComponent. - Data fetching should be asynchronous, preferably using RxJS Observables.
- The
fetchDatamethod inAbstractMetricComponentshould be abstract or protected, forcing subclasses to provide implementation. - Consider using Angular's
ChangeDetectionStrategy.OnPushfor performance optimization in metric components. - The
DashboardComponentshould be able to host multiple different metric components dynamically.
Notes
- Think about how to make the
AbstractMetricComponent's template reusable for common elements like loading spinners and error messages. You might consider using<ng-content>or defining protected template methods. - The
metricIdinput can be useful for debugging or for more advanced scenarios where metrics might need to interact or be referenced by a parent. - Consider how to handle complex data transformations or aggregations within the subclasses before passing data to the base component.
- This challenge focuses on the component structure and abstracting common logic. Real-world data fetching would typically involve Angular's
HttpClient.