Packages

Phoenix LiveView dashboard components for ExESDB cluster monitoring

Current section

Files

Jump to
ex_esdb_dashboard lib dashboard cluster_component.ex
Raw

lib/dashboard/cluster_component.ex

defmodule ExESDBDashboard.ClusterComponent do
@moduledoc """
LiveComponent for cluster view that can be embedded within other LiveViews.
This provides a reusable cluster dashboard component with real-time updates
via PubSub integration.
"""
use Phoenix.LiveComponent
@impl true
def mount(socket) do
{:ok, socket}
end
@impl true
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign_new(:cluster_data, fn -> get_cluster_data() end)
|> assign_new(:last_updated, fn -> DateTime.utc_now() end)
{:ok, socket}
end
@impl true
def handle_event("refresh", _params, socket) do
cluster_data = get_cluster_data()
socket =
socket
|> assign(:cluster_data, cluster_data)
|> assign(:last_updated, DateTime.utc_now())
# Send update to parent LiveView
send(self(), {:cluster_refreshed, cluster_data})
{:noreply, socket}
end
@impl true
def render(assigns) do
~H"""
<div class="cluster-component">
<div class="cluster-header">
<div class="header-content">
<h2>🏢 Cluster Overview</h2>
<p class="subtitle">Real-time cluster monitoring</p>
</div>
<div class="header-controls">
<button
type="button"
phx-click="refresh"
phx-target={@myself}
class="refresh-btn"
title="Refresh cluster data"
>
🔄 Refresh
</button>
</div>
</div>
<div class="cluster-content">
<!-- Cluster Health Overview -->
<div class="health-section">
<h3>📊 Cluster Health</h3>
<div class="health-indicator">
<div class={"health-status #{health_status_class(@cluster_data.cluster_health)}"}>
<span class="health-icon"><%= health_icon(@cluster_data.cluster_health) %></span>
<span class="health-text"><%= health_text(@cluster_data.cluster_health) %></span>
</div>
</div>
</div>
<!-- Cluster Nodes -->
<div class="nodes-section">
<h3>🖥️ Cluster Nodes</h3>
<div class="nodes-grid">
<%= for node <- @cluster_data.nodes do %>
<div class={"node-card #{node_status_class(node.status)}"}>
<div class="node-header">
<span class="node-name"><%= node.name %></span>
<span class="node-status"><%= node_status_text(node.status) %></span>
</div>
<div class="node-details">
<div class="node-detail">
<span class="detail-label">Type:</span>
<span class="detail-value"><%= if node.is_ex_esdb, do: "ExESDB", else: "Gater" %></span>
</div>
<div class="node-detail">
<span class="detail-label">Uptime:</span>
<span class="detail-value"><%= format_uptime(node.uptime) %></span>
</div>
<div class="node-detail">
<span class="detail-label">Last seen:</span>
<span class="detail-value"><%= format_datetime(node.last_seen) %></span>
</div>
</div>
</div>
<% end %>
</div>
</div>
<!-- Data Stores -->
<div class="stores-section">
<h3>🏪 Data Stores</h3>
<div class="stores-grid">
<%= for store <- @cluster_data.stores do %>
<div class="store-card">
<div class="store-header">
<span class="store-name"><%= store.name %></span>
<span class={"store-status #{store_status_class(store.status)}"}><%= store.status %></span>
</div>
<div class="store-stats">
<div class="store-stat">
<span class="stat-value"><%= store.stream_count %></span>
<span class="stat-label">Streams</span>
</div>
<div class="store-stat">
<span class="stat-value"><%= store.subscription_count %></span>
<span class="stat-label">Subscriptions</span>
</div>
<div class="store-stat">
<span class="stat-value"><%= length(store.nodes) %></span>
<span class="stat-label">Nodes</span>
</div>
</div>
</div>
<% end %>
</div>
</div>
<!-- Cluster Summary -->
<div class="summary-section">
<h3>📈 Cluster Summary</h3>
<div class="summary-stats">
<div class="summary-stat">
<span class="stat-value"><%= length(@cluster_data.nodes) %></span>
<span class="stat-label">Total Nodes</span>
</div>
<div class="summary-stat">
<span class="stat-value"><%= length(@cluster_data.stores) %></span>
<span class="stat-label">Data Stores</span>
</div>
<div class="summary-stat">
<span class="stat-value"><%= @cluster_data.total_streams %></span>
<span class="stat-label">Total Streams</span>
</div>
<div class="summary-stat">
<span class="stat-value"><%= format_datetime(@cluster_data.updated_at) %></span>
<span class="stat-label">Last Updated</span>
</div>
</div>
</div>
<!-- Last Update Info -->
<div class="update-info">
<p class="last-updated">
🕐 Last updated: <%= format_datetime(@last_updated) %>
</p>
</div>
</div>
</div>
"""
end
# Private helper functions
defp get_cluster_data do
try do
ExESDBDashboard.get_cluster_data()
rescue
_ ->
# Fallback data if ExESDBDashboard is not available
%{
nodes: [%{name: Node.self(), status: :self, is_ex_esdb: false, uptime: 0, last_seen: DateTime.utc_now()}],
stores: [],
total_streams: 0,
cluster_health: :no_cluster,
updated_at: DateTime.utc_now()
}
end
end
defp health_status_class(health) do
case health do
:healthy -> "healthy"
:degraded -> "degraded"
:unhealthy -> "unhealthy"
:no_cluster -> "no-cluster"
:no_stores -> "no-stores"
_ -> "unknown"
end
end
defp health_icon(health) do
case health do
:healthy -> "✅"
:degraded -> "⚠️"
:unhealthy -> "❌"
:no_cluster -> "🔍"
:no_stores -> "📦"
_ -> "❓"
end
end
defp health_text(health) do
case health do
:healthy -> "All systems operational"
:degraded -> "Some issues detected"
:unhealthy -> "Critical issues found"
:no_cluster -> "No cluster detected"
:no_stores -> "No data stores available"
_ -> "Health status unknown"
end
end
defp node_status_class(status) do
case status do
:connected -> "connected"
:self -> "self"
_ -> "unknown"
end
end
defp node_status_text(status) do
case status do
:connected -> "Connected"
:self -> "Local Node"
_ -> "Unknown"
end
end
defp store_status_class(status) do
case status do
:healthy -> "healthy"
:degraded -> "degraded"
:unhealthy -> "unhealthy"
_ -> "unknown"
end
end
defp format_uptime(uptime_ms) when is_integer(uptime_ms) do
seconds = div(uptime_ms, 1000)
cond do
seconds < 60 -> "#{seconds}s"
seconds < 3600 -> "#{div(seconds, 60)}m"
seconds < 86400 -> "#{div(seconds, 3600)}h"
true -> "#{div(seconds, 86400)}d"
end
end
defp format_uptime(_), do: "unknown"
defp format_datetime(%DateTime{} = dt) do
Calendar.strftime(dt, "%H:%M:%S")
end
defp format_datetime(_), do: "unknown"
end