Fixed 5 critical issues from code review: 1. **GlobalSearchTrigger hook listener leak** - Added destroyed() callback to remove click listener - Prevents memory leak on LiveView unmount 2. **NetworkMap/WeathermapViewer hook listener leaks** - Store zoom/fit button handlers as properties - Remove DOM event listeners in destroyed() callback - Fixes leak on every LiveView remount 3. **SitesMap uses undocumented window.liveSocket.execJS** - Changed to proper LiveView pattern using pushEvent - Attach listener via Leaflet's popupopen event 4. **handle_info(:refresh_data) double DB fetch** - Removed redundant Devices.get_device call - Reuse device from assign_base_data 5. **Redundant DB queries in DeviceLive.Show overview** - Changed load_sensor_chart_data to accept snmp_device - Changed load_overall_traffic_chart_data to accept snmp_device - Eliminated 4 redundant Snmp.get_device_with_associations calls Also removed unused functions: - load_equipment_data/2 - reload_active_tab_data/1 - assign_subscriber_impact/1 Note: DeviceLive.Show module size (2252 lines) is acknowledged but deferred as it requires larger architectural refactoring. Reviewed-on: graham/towerops-web#192
292 lines
12 KiB
Elixir
292 lines
12 KiB
Elixir
defmodule ToweropsWeb.DeviceLive.Components.ChecksTab do
|
|
@moduledoc """
|
|
LiveComponent for the device monitoring checks tab.
|
|
|
|
Displays all monitoring checks for the device grouped by type
|
|
(SNMP, HTTP, TCP, DNS, SSL, ping).
|
|
"""
|
|
use ToweropsWeb, :live_component
|
|
|
|
alias Towerops.Monitoring
|
|
alias ToweropsWeb.DeviceLive.Helpers.DataLoaders
|
|
|
|
@impl true
|
|
def update(assigns, socket) do
|
|
{:ok,
|
|
socket
|
|
|> assign(assigns)
|
|
|> load_checks_data()}
|
|
end
|
|
|
|
defp load_checks_data(socket) do
|
|
device_id = socket.assigns.device.id
|
|
organization_id = socket.assigns.device.organization_id
|
|
checks_data = DataLoaders.load_checks_data(device_id, organization_id)
|
|
|
|
socket
|
|
|> assign(:checks, checks_data.checks)
|
|
|> assign(:grouped_checks, checks_data.grouped_checks)
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<div id="checks-tab">
|
|
<%= if Enum.empty?(@checks) do %>
|
|
<!-- Empty State -->
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
|
|
<div class="px-4 py-12 text-center">
|
|
<.icon name="hero-clipboard-document-check" class="mx-auto h-12 w-12 text-gray-400" />
|
|
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("No checks configured")}
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{t(
|
|
"Run discovery to automatically detect sensors, interfaces, and other monitorable items, or add a service check manually."
|
|
)}
|
|
</p>
|
|
<div class="mt-6 flex justify-center gap-3">
|
|
<%= if @device.snmp_enabled do %>
|
|
<.button type="button" phx-click="run_discovery">
|
|
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Run Discovery
|
|
</.button>
|
|
<% end %>
|
|
<.button type="button" phx-click="add_check">
|
|
<.icon name="hero-plus" class="h-4 w-4" /> Add Check
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<!-- Checks List -->
|
|
<div class="space-y-6">
|
|
<!-- Header with count and Add button -->
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Checks ({length(@checks)})
|
|
</h2>
|
|
<.button type="button" phx-click="add_check">
|
|
<.icon name="hero-plus" class="h-4 w-4" /> Add Check
|
|
</.button>
|
|
</div>
|
|
<%= for {group_key, group_checks} <- @grouped_checks do %>
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
|
|
<!-- Group Header -->
|
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-gray-900/50">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{group_title(group_key)} ({length(group_checks)})
|
|
</h3>
|
|
</div>
|
|
<!-- Checks Table -->
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
|
|
<thead class="bg-gray-50 dark:bg-gray-900/30">
|
|
<tr>
|
|
<th
|
|
scope="col"
|
|
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
>
|
|
{t("Name")}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
>
|
|
{t("Status")}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
>
|
|
{t("Value")}
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
>
|
|
{t("Last Checked")}
|
|
</th>
|
|
<th scope="col" class="relative px-4 py-3">
|
|
<span class="sr-only">Actions</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
|
|
<%= for check <- group_checks do %>
|
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-900/30">
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
|
{check.name}
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-left">
|
|
{render_status_badge(check.current_state)}
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
<%= if check.last_check_at do %>
|
|
{get_latest_value(check)}
|
|
<% else %>
|
|
<span class="text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
|
Pending
|
|
</span>
|
|
<% end %>
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
<%= if check.last_check_at do %>
|
|
{format_relative_time(check.last_check_at)}
|
|
<% else %>
|
|
<span class="text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
|
Never
|
|
</span>
|
|
<% end %>
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
|
|
<div class="flex items-center justify-end gap-3">
|
|
<.link
|
|
navigate={
|
|
~p"/devices/#{@device.id}/graph/check?check_id=#{check.id}&range=24h"
|
|
}
|
|
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 text-sm font-medium"
|
|
>
|
|
{t("Graph")}
|
|
</.link>
|
|
<%= if check.check_type in ["http", "tcp", "dns", "ssl"] do %>
|
|
<button
|
|
type="button"
|
|
phx-click="edit_check"
|
|
phx-value-id={check.id}
|
|
class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
|
>
|
|
<.icon name="hero-pencil-square" class="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
phx-click="delete_check"
|
|
phx-value-id={check.id}
|
|
data-confirm="Are you sure you want to delete this check?"
|
|
class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
|
>
|
|
<.icon name="hero-trash" class="h-4 w-4" />
|
|
</button>
|
|
<% end %>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
# Helper functions
|
|
defp group_title(:snmp_sensors), do: "SNMP Sensors"
|
|
defp group_title(:snmp_interfaces), do: "SNMP Interfaces"
|
|
defp group_title(:snmp_processors), do: "SNMP Processors"
|
|
defp group_title(:snmp_storage), do: "SNMP Storage"
|
|
defp group_title(:http_checks), do: "HTTP Checks"
|
|
defp group_title(:tcp_checks), do: "TCP Checks"
|
|
defp group_title(:dns_checks), do: "DNS Checks"
|
|
defp group_title(:ssl_checks), do: "SSL Certificate Checks"
|
|
defp group_title(:ping_checks), do: "Ping Checks"
|
|
defp group_title(:other_checks), do: "Other Checks"
|
|
|
|
defp render_status_badge(0) do
|
|
assigns = %{}
|
|
|
|
~H"""
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
|
OK
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp render_status_badge(1) do
|
|
assigns = %{}
|
|
|
|
~H"""
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
|
|
WARNING
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp render_status_badge(2) do
|
|
assigns = %{}
|
|
|
|
~H"""
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
|
|
CRITICAL
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp render_status_badge(3) do
|
|
assigns = %{}
|
|
|
|
~H"""
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400">
|
|
UNKNOWN
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp render_status_badge(_) do
|
|
assigns = %{}
|
|
|
|
~H"""
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400">
|
|
PENDING
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp get_latest_value(check) do
|
|
# Get the latest check result to show current value
|
|
case Monitoring.get_latest_check_result(check.id) do
|
|
nil -> "-"
|
|
result -> format_check_value(result, check)
|
|
end
|
|
end
|
|
|
|
defp format_check_value(%{value: nil}, _check), do: "-"
|
|
|
|
defp format_check_value(%{value: value}, check) when is_number(value) do
|
|
case check.check_type do
|
|
"snmp_sensor" -> format_check_sensor_value(value, check.config)
|
|
"snmp_processor" -> "#{Float.round(value, 1)}%"
|
|
"snmp_storage" -> "#{Float.round(value, 1)}%"
|
|
t when t in ["ping", "http", "tcp", "dns"] -> "#{Float.round(value, 2)} ms"
|
|
_ -> value |> Float.round(2) |> to_string()
|
|
end
|
|
end
|
|
|
|
defp format_check_value(_result, _check), do: "-"
|
|
|
|
@sensor_value_formats %{
|
|
"temperature" => {1, "°C"},
|
|
"voltage" => {2, "V"},
|
|
"current" => {2, "A"},
|
|
"power" => {1, "W"},
|
|
"frequency" => {0, "Hz"},
|
|
"humidity" => {1, "%"}
|
|
}
|
|
|
|
defp format_check_sensor_value(value, %{"sensor_type" => "fanspeed", "sensor_unit" => unit}) do
|
|
"#{round(value)}#{unit || " RPM"}"
|
|
end
|
|
|
|
defp format_check_sensor_value(value, config) do
|
|
sensor_type = config["sensor_type"]
|
|
unit = config["sensor_unit"]
|
|
{precision, default_unit} = Map.get(@sensor_value_formats, sensor_type, {2, ""})
|
|
"#{Float.round(value, precision)}#{unit || default_unit}"
|
|
end
|
|
|
|
defp format_relative_time(datetime) do
|
|
ToweropsWeb.TimeHelpers.format_time_ago(datetime)
|
|
end
|
|
end
|