defmodule ExESDBDashboard.AdminLive do
@moduledoc """
Advanced Admin Dashboard LiveView for comprehensive cluster monitoring and management.
Features:
- Real-time cluster health monitoring
- Multi-tab navigation (Overview, Streams, Performance, Logs)
- Dark mode support with theme toggle
- Responsive design and mobile-friendly interface
- Live data updates with PubSub integration
- Stream analytics and performance metrics
- System logs with filtering capabilities
"""
use Phoenix.LiveView
require Logger
# Update interval for refreshing data (5 seconds)
@update_interval 5000
# Maximum number of log entries to keep
@max_log_entries 100
# Maximum number of activity entries to keep
@max_activity_entries 20
def mount(_params, _session, socket) do
# Subscribe to cluster events if PubSub is available
if connected?(socket) do
subscribe_to_cluster_events()
end
# Initialize socket state
socket =
socket
|> assign(:active_tab, "overview")
|> assign(:dark_mode, false)
|> assign(:auto_refresh, true)
|> assign(:last_updated, DateTime.utc_now())
|> assign(:cluster_data, get_initial_cluster_data())
|> assign(:stream_data, get_initial_stream_data())
|> assign(:performance_data, get_initial_performance_data())
|> assign(:logs, get_initial_logs())
|> assign(:activities, get_initial_activities())
|> assign(:log_level_filter, "all")
|> assign(:stream_search, "")
|> assign(:stream_filter, "all")
|> assign(:connection_status, :connected)
# Schedule automatic refresh if enabled
if connected?(socket) and socket.assigns.auto_refresh do
Process.send_after(self(), :refresh_data, @update_interval)
end
{:ok, socket}
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end
# Handle tab navigation
def handle_event("switch_tab", %{"tab" => tab}, socket) do
{:noreply, assign(socket, :active_tab, tab)}
end
# Handle dark mode toggle
def handle_event("toggle_dark_mode", _params, socket) do
new_dark_mode = !socket.assigns.dark_mode
{:noreply, assign(socket, :dark_mode, new_dark_mode)}
end
# Handle auto-refresh toggle
def handle_event("toggle_auto_refresh", _params, socket) do
new_auto_refresh = !socket.assigns.auto_refresh
socket = assign(socket, :auto_refresh, new_auto_refresh)
# Schedule or cancel refresh based on new setting
if new_auto_refresh and connected?(socket) do
Process.send_after(self(), :refresh_data, @update_interval)
end
{:noreply, socket}
end
# Handle manual refresh
def handle_event("refresh", _params, socket) do
{:noreply, refresh_all_data(socket)}
end
# Handle log level filter change
def handle_event("filter_logs", %{"level" => level}, socket) do
{:noreply, assign(socket, :log_level_filter, level)}
end
# Handle stream search
def handle_event("search_streams", %{"search" => search}, socket) do
{:noreply, assign(socket, :stream_search, search)}
end
# Handle stream filter change
def handle_event("filter_streams", %{"filter" => filter}, socket) do
{:noreply, assign(socket, :stream_filter, filter)}
end
# Handle periodic data refresh
def handle_info(:refresh_data, socket) do
socket =
if socket.assigns.auto_refresh do
# Schedule next refresh
Process.send_after(self(), :refresh_data, @update_interval)
refresh_all_data(socket)
else
socket
end
{:noreply, socket}
end
# Handle cluster health updates from PubSub
def handle_info({:cluster_health_update, health_data}, socket) do
cluster_data = Map.merge(socket.assigns.cluster_data, %{health: health_data})
activities = add_activity(socket.assigns.activities, "π₯", "Cluster health updated", "now")
socket =
socket
|> assign(:cluster_data, cluster_data)
|> assign(:activities, activities)
|> assign(:last_updated, DateTime.utc_now())
{:noreply, socket}
end
# Handle cluster lifecycle events
def handle_info({:cluster_lifecycle, event_data}, socket) do
activities = add_activity(socket.assigns.activities, "π", "Cluster lifecycle event: #{event_data.type}", "now")
socket =
socket
|> assign(:activities, activities)
|> assign(:last_updated, DateTime.utc_now())
{:noreply, socket}
end
# Handle system events
def handle_info({:system_event, event_data}, socket) do
activities = add_activity(socket.assigns.activities, "β‘", "System event: #{event_data.type}", "now")
socket =
socket
|> assign(:activities, activities)
|> assign(:last_updated, DateTime.utc_now())
{:noreply, socket}
end
# Handle connection status changes
def handle_info({:connection_status, status}, socket) do
{:noreply, assign(socket, :connection_status, status)}
end
# Catch-all for unknown messages
def handle_info(msg, socket) do
Logger.debug("AdminLive received unknown message: #{inspect(msg)}")
{:noreply, socket}
end
# Template rendering
def render(assigns) do
~H"""
<%= case @active_tab do %>
<% "overview" -> %>
<%= render_overview_tab(assigns) %>
<% "streams" -> %>
<%= render_streams_tab(assigns) %>
<% "performance" -> %>
<%= render_performance_tab(assigns) %>
<% "logs" -> %>
<%= render_logs_tab(assigns) %>
<% end %>
"""
end
# Overview tab template
defp render_overview_tab(assigns) do
~H"""
π₯ Cluster Health
<%= @cluster_data.health.emoji %>
<%= @cluster_data.health.message %>
Last checked: <%= format_datetime(@last_updated) %>
<%= @cluster_data.nodes_count %>
Active Nodes
<%= @cluster_data.stores_count %>
Data Stores
π System Metrics
πΎ
<%= @cluster_data.memory_usage %>%
Memory Usage
β‘
<%= @cluster_data.cpu_usage %>%
CPU Usage
π
<%= @cluster_data.connections %>
Active Connections
π Recent Activity
<%= for activity <- Enum.take(@activities, 5) do %>
<%= activity.icon %>
<%= activity.message %>
<%= activity.time %>
<% end %>
Last updated: <%= format_datetime(@last_updated) %>
(<%= connection_status_text(@connection_status) %>)
"""
end
# Streams tab template
defp render_streams_tab(assigns) do
~H"""
π Stream Analytics
<%= @stream_data.total_streams %>
Total Streams
<%= @stream_data.active_streams %>
Active
<%= @stream_data.total_events %>
Total Events
π Top Streams
<%= for stream <- @stream_data.top_streams do %>
<%= stream.name %>
<%= stream.event_count %> events
<% end %>
"""
end
# Performance tab template
defp render_performance_tab(assigns) do
~H"""
"""
end
# Logs tab template
defp render_logs_tab(assigns) do
assigns = assign(assigns, :filtered_logs, filter_logs(assigns.logs, assigns.log_level_filter))
~H"""
<%= if length(@filtered_logs) > 0 do %>
<%= for log <- @filtered_logs do %>
<%= log.timestamp %>
<%= String.upcase(log.level) %>
<%= log.message %>
<% end %>
<% else %>
No logs found for the selected filter.
<% end %>
"""
end
# Data fetching functions
defp get_initial_cluster_data do
# Try to get data from ExESDBDashboard if available
try do
health = ExESDBDashboard.cluster_health()
nodes = ExESDBDashboard.cluster_nodes()
stores = ExESDBDashboard.cluster_stores()
%{
health: format_health_data(health),
nodes_count: length(nodes),
stores_count: length(stores),
memory_usage: :rand.uniform(20) + 60, # Simulated: 60-80%
cpu_usage: :rand.uniform(15) + 25, # Simulated: 25-40%
connections: :rand.uniform(50) + 100 # Simulated: 100-150
}
rescue
_ ->
# Fallback to simulated data if ExESDBDashboard functions aren't available
%{
health: %{status: "healthy", emoji: "β
", message: "All systems operational"},
nodes_count: 3,
stores_count: 5,
memory_usage: 72,
cpu_usage: 35,
connections: 127
}
end
end
defp get_initial_stream_data do
%{
total_streams: :rand.uniform(100) + 200,
active_streams: :rand.uniform(50) + 150,
total_events: :rand.uniform(10000) + 50000,
top_streams: [
%{name: "user-events", event_count: 15432},
%{name: "order-processing", event_count: 12876},
%{name: "inventory-updates", event_count: 9543},
%{name: "user-analytics", event_count: 8721},
%{name: "payment-events", event_count: 7654}
]
}
end
defp get_initial_performance_data do
%{
response_time: :rand.uniform(50) + 25,
response_time_trend: Enum.random(["trend-up", "trend-down", "trend-stable"]),
throughput: :rand.uniform(500) + 1000,
throughput_trend: Enum.random(["trend-up", "trend-down", "trend-stable"]),
error_rate: :rand.uniform(3) + 1,
error_rate_trend: Enum.random(["trend-up", "trend-down", "trend-stable"]),
memory_usage: :rand.uniform(200) + 800,
memory_trend: Enum.random(["trend-up", "trend-down", "trend-stable"])
}
end
defp get_initial_logs do
base_time = DateTime.utc_now()
[
%{
timestamp: DateTime.to_string(DateTime.add(base_time, -300)),
level: "info",
message: "Cluster health check completed successfully"
},
%{
timestamp: DateTime.to_string(DateTime.add(base_time, -180)),
level: "warning",
message: "High memory usage detected on node esdb-2"
},
%{
timestamp: DateTime.to_string(DateTime.add(base_time, -120)),
level: "info",
message: "Stream 'user-events' processed 1000 new events"
},
%{
timestamp: DateTime.to_string(DateTime.add(base_time, -60)),
level: "error",
message: "Connection timeout to store esdb-store-3"
},
%{
timestamp: DateTime.to_string(base_time),
level: "info",
message: "Admin dashboard accessed from 192.168.1.100"
}
]
end
defp get_initial_activities do
[
%{icon: "π₯", message: "Cluster health check completed", time: "2 minutes ago"},
%{icon: "π", message: "Node esdb-3 joined the cluster", time: "5 minutes ago"},
%{icon: "β‘", message: "Stream processing resumed", time: "10 minutes ago"},
%{icon: "π", message: "Performance metrics updated", time: "15 minutes ago"},
%{icon: "π§", message: "System configuration reloaded", time: "20 minutes ago"}
]
end
# Helper functions
defp subscribe_to_cluster_events do
topics = [
"cluster_health_updates",
"cluster_lifecycle_events",
"system_events",
"connection_status"
]
Enum.each(topics, fn topic ->
try do
Phoenix.PubSub.subscribe(ExESDBDashboard.PubSub, topic)
Logger.debug("AdminLive subscribed to #{topic} on ExESDBDashboard.PubSub")
rescue
error ->
Logger.warning("AdminLive failed to subscribe to #{topic}: #{inspect(error)}")
end
end)
end
defp refresh_all_data(socket) do
socket
|> assign(:cluster_data, get_initial_cluster_data())
|> assign(:stream_data, get_initial_stream_data())
|> assign(:performance_data, get_initial_performance_data())
|> assign(:last_updated, DateTime.utc_now())
end
defp add_activity(activities, icon, message, time) do
new_activity = %{icon: icon, message: message, time: time}
[new_activity | activities]
|> Enum.take(@max_activity_entries)
end
defp format_health_data(health) when is_atom(health) do
case health do
:healthy -> %{status: "healthy", emoji: "β
", message: "All systems operational"}
:degraded -> %{status: "degraded", emoji: "β οΈ", message: "Some issues detected"}
:unhealthy -> %{status: "unhealthy", emoji: "β", message: "Critical issues found"}
_ -> %{status: "unknown", emoji: "β", message: "Health status unknown"}
end
end
defp format_health_data(health), do: health
defp format_datetime(datetime) do
datetime
|> DateTime.truncate(:second)
|> DateTime.to_string()
|> String.replace("Z", " UTC")
end
defp format_trend(trend) do
case trend do
"trend-up" -> "βοΈ Up"
"trend-down" -> "βοΈ Down"
"trend-stable" -> "β‘οΈ Stable"
_ -> "β‘οΈ Stable"
end
end
defp connection_status_text(status) do
case status do
:connected -> "Connected"
:disconnected -> "Disconnected"
:reconnecting -> "Reconnecting..."
_ -> "Unknown"
end
end
defp filter_logs(logs, "all"), do: logs
defp filter_logs(logs, level), do: Enum.filter(logs, &(&1.level == level))
end