343 lines
15 KiB
Plaintext
343 lines
15 KiB
Plaintext
<% content_for :title, "Analytics Dashboard - Baffle Hub" %>
|
|
|
|
<div class="space-y-6" data-controller="dashboard" data-dashboard-period-value="<%= @time_period %>">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">Analytics Dashboard</h1>
|
|
<p class="mt-2 text-gray-600">Overview of WAF events, rules, and network activity</p>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-4">
|
|
<!-- Auto-refresh indicator -->
|
|
<div class="flex items-center text-sm text-gray-500">
|
|
<svg class="animate-spin h-4 w-4 mr-2 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Auto-refreshing
|
|
</div>
|
|
|
|
<!-- Time Period Selector -->
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-sm text-gray-500">Time Period:</span>
|
|
<div class="flex bg-white rounded-md shadow-sm border border-gray-300">
|
|
<%= link_to "1H", analytics_path(period: :hour),
|
|
class: time_period_class(:hour),
|
|
data: { action: "click->dashboard#periodChanged", period: "hour" } %>
|
|
<%= link_to "24H", analytics_path(period: :day),
|
|
class: time_period_class(:day),
|
|
data: { action: "click->dashboard#periodChanged", period: "day" } %>
|
|
<%= link_to "1W", analytics_path(period: :week),
|
|
class: time_period_class(:week),
|
|
data: { action: "click->dashboard#periodChanged", period: "week" } %>
|
|
<%= link_to "1M", analytics_path(period: :month),
|
|
class: time_period_class(:month),
|
|
data: { action: "click->dashboard#periodChanged", period: "month" } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Key Statistics Cards -->
|
|
<div class="space-y-6">
|
|
<div id="dashboard-stats" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<!-- Total Events -->
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93 0 3.21-1.92 6-4.72 7.28l-.28.12V22l-5-5 5-5v2.4c2.21-.47 3.88-2.35 3.88-4.65 0-2.76-2.24-5-5-5-1.3 0-2.47.5-3.36 1.31L9 6.36C10.11 5.26 11.49 4.56 13 4.56V2.05c0-.45.54-.67.85-.35l8.78 8.78c.2.2.2.51 0 .71l-8.78 8.78c-.31.31-.85.1-.85-.35v-2.02c-5.05-.5-9-4.76-9-9.93 0-4.08 2.73-7.54 6.58-8.67L13 2.05z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Total Events</dt>
|
|
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_events) %></dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-50 px-5 py-3">
|
|
<div class="text-sm">
|
|
<span class="text-green-600 font-medium">Last <%= @time_period.to_s.humanize %></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Rules -->
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Active Rules</dt>
|
|
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_rules) %></dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-50 px-5 py-3">
|
|
<div class="text-sm">
|
|
<span class="text-green-600 font-medium">Enabled</span>
|
|
<% if @system_health[:disabled_rules] > 0 %>
|
|
<span class="text-gray-500"> · <%= @system_health[:disabled_rules] %> disabled</span>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Ranges with Events -->
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Active Network Ranges</dt>
|
|
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@network_ranges_with_events) %></dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-50 px-5 py-3">
|
|
<div class="text-sm">
|
|
<span class="text-purple-600 font-medium">of <%= number_with_delimiter(@total_network_ranges) %> total</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Health -->
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-orange-500 rounded-md flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">System Health</dt>
|
|
<dd class="text-lg font-medium text-gray-900">Normal</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-50 px-5 py-3">
|
|
<div class="text-sm">
|
|
<span class="text-green-600 font-medium">All systems operational</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts and Detailed Analytics -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Events Timeline Chart -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Events Timeline (Last 24 Hours)</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="space-y-4">
|
|
<% @chart_data[:timeline].each do |data| %>
|
|
<div class="flex items-center">
|
|
<div class="w-16 text-sm text-gray-500"><%= data[:time] %></div>
|
|
<div class="flex-1 mx-4">
|
|
<div class="bg-gray-200 rounded-full h-4">
|
|
<div class="bg-blue-600 h-4 rounded-full"
|
|
style="width: <%= [((data[:total].to_f / [@chart_data[:timeline].map { |d| d[:total] }.max, 1].max) * 100), 5].max %>%">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="w-12 text-sm text-gray-900 text-right"><%= data[:total] %></div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Event Actions Breakdown -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Event Actions</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<% if @chart_data[:actions].any? %>
|
|
<div class="space-y-4">
|
|
<% @chart_data[:actions].each do |action| %>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 rounded mr-3
|
|
<%= case action[:action]
|
|
when 'Allow' then 'bg-green-500'
|
|
when 'Deny', 'Block' then 'bg-red-500'
|
|
when 'Challenge' then 'bg-yellow-500'
|
|
else 'bg-gray-500'
|
|
end %>">
|
|
</div>
|
|
<span class="text-sm font-medium text-gray-900"><%= action[:action] %></span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<span class="text-sm text-gray-600 mr-2"><%= number_with_delimiter(action[:count]) %></span>
|
|
<span class="text-sm text-gray-500">(<%= action[:percentage] %>%)</span>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<p class="text-gray-500 text-center py-8">No events in the selected time period</p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Secondary Information Rows -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Top Countries -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Top Countries</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<% if @top_countries.any? %>
|
|
<div class="space-y-3">
|
|
<% @top_countries.first(5).each do |country, count| %>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-900"><%= country %></span>
|
|
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(count) %></span>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<p class="text-gray-500 text-center py-4">No country data available</p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Intelligence -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Network Intelligence</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-900">🏢 Datacenter</span>
|
|
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:datacenter_ranges]) %></span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-900">🔒 VPN</span>
|
|
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:vpn_ranges]) %></span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-900">🛡️ Proxy</span>
|
|
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:proxy_ranges]) %></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<div id="recent-activity" class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Recent Activity</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="space-y-3">
|
|
<% @recent_events.first(3).each do |event| %>
|
|
<div class="flex items-center justify-between text-sm">
|
|
<div class="flex items-center">
|
|
<div class="w-2 h-2 rounded-full mr-2
|
|
<%= event.waf_action == 'allow' ? 'bg-green-500' : 'bg-red-500' %>"></div>
|
|
<span class="text-gray-900 truncate max-w-[120px]"><%= event.ip_address %></span>
|
|
</div>
|
|
<span class="text-gray-500"><%= time_ago_in_words(event.timestamp) %> ago</span>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Blocked IPs -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-medium text-gray-900">Top Blocked IPs</h3>
|
|
<%= link_to "View All Events", events_path, class: "text-sm text-blue-600 hover:text-blue-800" %>
|
|
</div>
|
|
</div>
|
|
<div class="p-6">
|
|
<% if @top_blocked_ips.any? %>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
<% @top_blocked_ips.each do |ip, count| %>
|
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
|
<div class="text-lg font-mono font-medium text-gray-900"><%= ip %></div>
|
|
<div class="text-sm text-red-600"><%= number_with_delimiter(count) %> blocks</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<p class="text-gray-500 text-center py-8">No blocked events in the selected time period</p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Quick Actions</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<%= link_to new_rule_path, class: "flex items-center justify-center px-4 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" do %>
|
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
|
</svg>
|
|
Create Rule
|
|
<% end %>
|
|
|
|
<%= link_to new_network_range_path, class: "flex items-center justify-center px-4 py-3 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors" do %>
|
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
|
</svg>
|
|
Add Network Range
|
|
<% end %>
|
|
|
|
<%= link_to events_path, class: "flex items-center justify-center px-4 py-3 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors" do %>
|
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
</svg>
|
|
View Events
|
|
<% end %>
|
|
|
|
<%= link_to rules_path, class: "flex items-center justify-center px-4 py-3 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors" do %>
|
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
|
</svg>
|
|
Manage Rules
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div> |