Current section
Files
Jump to
Current section
Files
lib/dashboard/admin_live.ex
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"""
<div class={"admin-dashboard #{if @dark_mode, do: "dark-mode", else: ""}"}>
<div class="dashboard-header">
<div class="header-content">
<div class="title-section">
<h1>
🏢 ReckonDB Admin Dashboard
</h1>
<p class="subtitle">
Advanced cluster monitoring and management
</p>
</div>
<div class="header-controls">
<div class="control-group">
<button
class={"toggle-btn #{if @auto_refresh, do: "active", else: ""}"}
phx-click="toggle_auto_refresh"
>
<%= if @auto_refresh do %>
⏸️ Pause
<% else %>
▶️ Resume
<% end %>
</button>
<button class="refresh-btn" phx-click="refresh">
🔄 Refresh
</button>
<button
class={"toggle-btn #{if @dark_mode, do: "active", else: ""}"}
phx-click="toggle_dark_mode"
>
<%= if @dark_mode do %>
☀️ Light
<% else %>
🌙 Dark
<% end %>
</button>
</div>
</div>
</div>
<nav class="dashboard-nav">
<button
class={"nav-tab #{if @active_tab == "overview", do: "active", else: ""}"}
phx-click="switch_tab"
phx-value-tab="overview"
>
📊 Overview
</button>
<button
class={"nav-tab #{if @active_tab == "streams", do: "active", else: ""}"}
phx-click="switch_tab"
phx-value-tab="streams"
>
🌊 Streams
</button>
<button
class={"nav-tab #{if @active_tab == "performance", do: "active", else: ""}"}
phx-click="switch_tab"
phx-value-tab="performance"
>
📈 Performance
</button>
<button
class={"nav-tab #{if @active_tab == "logs", do: "active", else: ""}"}
phx-click="switch_tab"
phx-value-tab="logs"
>
📝 Logs
</button>
</nav>
</div>
<div class="dashboard-content">
<%= 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 %>
</div>
</div>
"""
end
# Overview tab template
defp render_overview_tab(assigns) do
~H"""
<div class="tab-content">
<div class="overview-grid">
<!-- Cluster Health Overview -->
<div class="health-overview-card">
<h3>🏥 Cluster Health</h3>
<div class={"health-indicator #{@cluster_data.health.status}"}>
<div class="health-icon"><%= @cluster_data.health.emoji %></div>
<div>
<div class="health-title"><%= @cluster_data.health.message %></div>
<div class="health-message">
Last checked: <%= format_datetime(@last_updated) %>
</div>
</div>
</div>
<div class="quick-stats">
<div class="stat-item">
<span class="stat-value"><%= @cluster_data.nodes_count %></span>
<div class="stat-label">Active Nodes</div>
</div>
<div class="stat-item">
<span class="stat-value"><%= @cluster_data.stores_count %></span>
<div class="stat-label">Data Stores</div>
</div>
</div>
</div>
<!-- System Metrics -->
<div class="metrics-card">
<h3>📊 System Metrics</h3>
<div class="metrics-grid">
<div class="metric-item">
<div class="metric-icon">💾</div>
<div class="metric-info">
<span class="metric-value"><%= @cluster_data.memory_usage %>%</span>
<div class="metric-label">Memory Usage</div>
</div>
</div>
<div class="metric-item">
<div class="metric-icon">⚡</div>
<div class="metric-info">
<span class="metric-value"><%= @cluster_data.cpu_usage %>%</span>
<div class="metric-label">CPU Usage</div>
</div>
</div>
<div class="metric-item">
<div class="metric-icon">🔌</div>
<div class="metric-info">
<span class="metric-value"><%= @cluster_data.connections %></span>
<div class="metric-label">Active Connections</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="activity-card">
<h3>📋 Recent Activity</h3>
<div class="activity-list">
<%= for activity <- Enum.take(@activities, 5) do %>
<div class="activity-item">
<div class="activity-icon"><%= activity.icon %></div>
<div class="activity-content">
<span class="activity-text"><%= activity.message %></span>
<div class="activity-time"><%= activity.time %></div>
</div>
</div>
<% end %>
</div>
</div>
<!-- Update Information -->
<div class="update-info">
<p class="last-updated">
Last updated: <%= format_datetime(@last_updated) %>
(<%= connection_status_text(@connection_status) %>)
</p>
</div>
</div>
</div>
"""
end
# Streams tab template
defp render_streams_tab(assigns) do
~H"""
<div class="tab-content">
<div class="streams-dashboard">
<div class="streams-header">
<h2>🌊 Stream Analytics</h2>
<div class="stream-controls">
<input
type="text"
class="search-input"
placeholder="Search streams..."
value={@stream_search}
phx-change="search_streams"
/>
<select class="filter-select" phx-change="filter_streams">
<option value="all">All Streams</option>
<option value="active">Active Only</option>
<option value="recent">Recently Updated</option>
</select>
</div>
</div>
<div class="streams-grid">
<div class="analytics-card">
<h3>📈 Stream Analytics</h3>
<div class="analytics-stats">
<div class="analytics-stat">
<span class="stat-value"><%= @stream_data.total_streams %></span>
<div class="stat-label">Total Streams</div>
</div>
<div class="analytics-stat">
<span class="stat-value"><%= @stream_data.active_streams %></span>
<div class="stat-label">Active</div>
</div>
<div class="analytics-stat">
<span class="stat-value"><%= @stream_data.total_events %></span>
<div class="stat-label">Total Events</div>
</div>
</div>
</div>
<div class="top-streams-card">
<h3>🏆 Top Streams</h3>
<div class="stream-list">
<%= for stream <- @stream_data.top_streams do %>
<div class="stream-item">
<div class="stream-name"><%= stream.name %></div>
<div class="stream-count"><%= stream.event_count %> events</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
"""
end
# Performance tab template
defp render_performance_tab(assigns) do
~H"""
<div class="tab-content">
<div class="performance-dashboard">
<div class="metrics-overview">
<h2>📈 Performance Metrics</h2>
<div class="performance-cards">
<div class="perf-card">
<h4>Response Time</h4>
<div class="metric-display">
<span class="metric-value"><%= @performance_data.response_time %>ms</span>
<span class={"metric-trend #{@performance_data.response_time_trend}"}><%= format_trend(@performance_data.response_time_trend) %></span>
</div>
</div>
<div class="perf-card">
<h4>Throughput</h4>
<div class="metric-display">
<span class="metric-value"><%= @performance_data.throughput %>/sec</span>
<span class={"metric-trend #{@performance_data.throughput_trend}"}><%= format_trend(@performance_data.throughput_trend) %></span>
</div>
</div>
<div class="perf-card">
<h4>Error Rate</h4>
<div class="metric-display">
<span class="metric-value"><%= @performance_data.error_rate %>%</span>
<span class={"metric-trend #{@performance_data.error_rate_trend}"}><%= format_trend(@performance_data.error_rate_trend) %></span>
</div>
</div>
<div class="perf-card">
<h4>Memory Usage</h4>
<div class="metric-display">
<span class="metric-value"><%= @performance_data.memory_usage %>MB</span>
<span class={"metric-trend #{@performance_data.memory_trend}"}><%= format_trend(@performance_data.memory_trend) %></span>
</div>
</div>
</div>
</div>
<div class="charts-section">
<div class="chart-card">
<h3>📊 Performance Charts</h3>
<div class="chart-placeholder">
<div>📈</div>
<p>Performance charts will be displayed here</p>
<div class="chart-note">Charts can be integrated with libraries like LiveChart or Chart.js</div>
</div>
</div>
</div>
</div>
</div>
"""
end
# Logs tab template
defp render_logs_tab(assigns) do
assigns = assign(assigns, :filtered_logs, filter_logs(assigns.logs, assigns.log_level_filter))
~H"""
<div class="tab-content">
<div class="logs-dashboard">
<div class="logs-header">
<h2>📝 System Logs</h2>
<select class="log-level-filter" phx-change="filter_logs">
<option value="all">All Levels</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
</select>
</div>
<div class="logs-content">
<%= if length(@filtered_logs) > 0 do %>
<%= for log <- @filtered_logs do %>
<div class="log-entry">
<div class="log-timestamp"><%= log.timestamp %></div>
<div class={"log-level level-#{log.level}"}><%= String.upcase(log.level) %></div>
<div class="log-message"><%= log.message %></div>
</div>
<% end %>
<% else %>
<div class="logs-empty">
<p>No logs found for the selected filter.</p>
</div>
<% end %>
</div>
</div>
</div>
"""
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