Hone logo
Hone
Problems

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:

  1. Abstract Base Component: Create an abstract class AbstractMetricComponent that defines common properties and methods for all metric components.
  2. 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.
  3. Display Logic: The base component should handle common display logic, such as loading states, error handling, and rendering the actual metric value.
  4. Input Properties: Define input properties for the base component that are common to all metrics (e.g., a title, a unique identifier).
  5. Specific Metric Component: Implement at least one concrete metric component (e.g., UserCountMetricComponent) that extends AbstractMetricComponent and provides its own data fetching and display logic.
  6. Dashboard Integration: Create a simple DashboardComponent that can host multiple instances of different AbstractMetricComponent subclasses.

Expected Behavior:

  • When the DashboardComponent loads, each AbstractMetricComponent instance 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 AbstractMetricComponent must be an abstract class.
  • All concrete metric components must extend AbstractMetricComponent.
  • Data fetching should be asynchronous, preferably using RxJS Observables.
  • The fetchData method in AbstractMetricComponent should be abstract or protected, forcing subclasses to provide implementation.
  • Consider using Angular's ChangeDetectionStrategy.OnPush for performance optimization in metric components.
  • The DashboardComponent should 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 metricId input 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.
Loading editor...
typescript