Dashboard React pour visualiser en temps réel l’état de santé de 45+ services déployés sur Nomad, via l’API Consul. Filtrage par catégorie, recherche, et statut live.

Contexte
Migration d’une plateforme data de Docker Compose vers Nomad/Consul. Besoin d’une vue d’ensemble des services déployés sans passer par les UI natives de Nomad et Consul.
Stack technique
| Composant | Technologie |
|---|---|
| Frontend | React 19 + TypeScript + Vite |
| Styling | Tailwind CSS |
| Backend | API Consul (health checks) |
| Orchestration | HashiCorp Nomad |
| Discovery | HashiCorp Consul |
Architecture
┌─────────────────┐ ┌─────────────────┐
│ React Dashboard │────▶│ Consul API │
│ (Vite + TS) │ │ /v1/health/* │
└─────────────────┘ └────────┬────────┘
│
┌────────▼────────┐
│ Nomad │
│ (25+ jobs) │
└─────────────────┘
Le dashboard interroge Consul toutes les 10 secondes pour récupérer l’état des health checks. Chaque service Nomad s’enregistre automatiquement dans Consul avec ses checks TCP/HTTP.
Catalogue de services
Le dashboard catégorise 45 services en 8 catégories :
export const SERVICE_CATALOG: ServiceInfo[] = [
// Databases
{ id: 'postgres', name: 'PostgreSQL', category: 'Databases', port: 5432 },
{ id: 'mongodb', name: 'MongoDB', category: 'Databases', port: 27017 },
{ id: 'neo4j', name: 'Neo4j', category: 'Databases', port: 7474, hasUI: true },
{ id: 'influxdb', name: 'InfluxDB', category: 'Databases', port: 8086, hasUI: true },
{ id: 'redis', name: 'Redis', category: 'Databases', port: 6379 },
// Streaming
{ id: 'kafka', name: 'Kafka', category: 'Streaming', port: 9092 },
{ id: 'zookeeper', name: 'Zookeeper', category: 'Streaming', port: 2181 },
// APIs
{ id: 'fastapi', name: 'FastAPI', category: 'APIs', port: 8000, hasUI: true },
{ id: 'graphql', name: 'GraphQL', category: 'APIs', port: 4000, hasUI: true },
// Orchestration
{ id: 'airflow-webserver', name: 'Airflow', category: 'Orchestration', port: 8080, hasUI: true },
// Admin Tools
{ id: 'pgadmin', name: 'pgAdmin', category: 'Admin Tools', port: 5050, hasUI: true },
{ id: 'mongo-express', name: 'Mongo Express', category: 'Admin Tools', port: 8081, hasUI: true },
{ id: 'kafka-ui', name: 'Kafka UI', category: 'Admin Tools', port: 9080, hasUI: true },
{ id: 'grafana', name: 'Grafana', category: 'Admin Tools', port: 3000, hasUI: true },
// Infrastructure
{ id: 'nomad', name: 'Nomad', category: 'Infrastructure', port: 4646, hasUI: true },
{ id: 'consul', name: 'Consul', category: 'Infrastructure', port: 8500, hasUI: true },
{ id: 'traefik', name: 'Traefik', category: 'Infrastructure', port: 8080, hasUI: true },
// Security
{ id: 'keycloak', name: 'Keycloak', category: 'Security & IAM', port: 8180, hasUI: true },
{ id: 'vault', name: 'Vault', category: 'Security & IAM', port: 8200, hasUI: true },
];Hook de health check
import { useState, useEffect } from 'react';
interface HealthCheck {
ServiceID: string;
Status: string;
Node: string;
}
export function useConsulHealth(refreshInterval = 10000) {
const [healthData, setHealthData] = useState<Record<string, boolean>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchHealth = async () => {
try {
const response = await fetch('/api/consul/v1/health/state/passing');
const checks: HealthCheck[] = await response.json();
const healthMap: Record<string, boolean> = {};
checks.forEach(check => {
healthMap[check.ServiceID] = check.Status === 'passing';
});
setHealthData(healthMap);
} catch (error) {
console.error('Failed to fetch health data:', error);
} finally {
setLoading(false);
}
};
fetchHealth();
const interval = setInterval(fetchHealth, refreshInterval);
return () => clearInterval(interval);
}, [refreshInterval]);
return { healthData, loading };
}Composant ServiceCard
interface ServiceCardProps {
service: ServiceInfo;
isOnline: boolean;
host: string;
}
export function ServiceCard({ service, isOnline, host }: ServiceCardProps) {
const statusColor = isOnline ? 'bg-green-500' : 'bg-red-500';
const url = service.hasUI ? `http://${host}:${service.port}` : null;
return (
<div className="bg-white rounded-lg shadow p-4 hover:shadow-md transition">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<ServiceIcon category={service.category} />
<div>
<h3 className="font-semibold">{service.name}</h3>
<p className="text-sm text-gray-500">:{service.port}</p>
</div>
</div>
<span className={`w-3 h-3 rounded-full ${statusColor}`} />
</div>
{url && (
<a href={url} target="_blank" className="text-blue-600 text-sm mt-2 block">
Ouvrir →
</a>
)}
</div>
);
}
Filtrage et recherche
Le dashboard permet de :
- Filtrer par catégorie : Databases, APIs, Streaming, etc.
- Rechercher par nom, ID, catégorie ou port
- Voir les stats : nombre de services online/offline par catégorie
Déploiement
Le dashboard est lui-même déployé sur Nomad :
job "infra-dashboard" {
type = "service"
group "dashboard" {
network {
port "http" {
static = 3001
}
}
task "dashboard" {
driver = "docker"
config {
image = "registry:5000/infra-dashboard:latest"
ports = ["http"]
}
resources {
cpu = 100
memory = 128
}
service {
name = "infra-dashboard"
port = "http"
check {
type = "http"
path = "/"
interval = "10s"
timeout = "2s"
}
}
}
}
}
Résultat
Le dashboard affiche en temps réel :
- 45 services répartis en 8 catégories
- Statut online/offline avec indicateur visuel
- Liens directs vers les UI (Grafana, pgAdmin, Consul, etc.)
- Compteurs par catégorie et global
Nomad UI
L’UI native de Nomad reste accessible pour le détail des jobs et allocations :

Détail d’un job complexe comme Airflow (5 task groups) :

Consul API · Nomad · React 19