272 lines
9.2 KiB
Elixir
272 lines
9.2 KiB
Elixir
defmodule ToweropsWeb.TraceLive.Index do
|
|
@moduledoc """
|
|
LiveView for the Subscriber Trace / Help Desk Mode.
|
|
|
|
Provides a unified search across subscribers, devices, and access points,
|
|
then assembles a full "patient chart" trace view showing all relevant
|
|
network health information at a glance.
|
|
"""
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Trace
|
|
|
|
attr :trace, :map, required: true
|
|
|
|
defp issues_callout(assigns) do
|
|
issues = []
|
|
|
|
issues =
|
|
if assigns.trace[:device] && assigns.trace.device.status == :down, do: [t("Device is DOWN") | issues], else: issues
|
|
|
|
issues =
|
|
if assigns.trace[:qoe_metrics] && assigns.trace.qoe_metrics[:qoe_score] && assigns.trace.qoe_metrics.qoe_score < 50,
|
|
do: ["QoE score is critical (#{assigns.trace.qoe_metrics.qoe_score})" | issues],
|
|
else: issues
|
|
|
|
issues =
|
|
if assigns.trace[:active_alerts] && assigns.trace.active_alerts != [],
|
|
do: ["#{length(assigns.trace.active_alerts)} active alert(s)" | issues],
|
|
else: issues
|
|
|
|
assigns = assign(assigns, :issues, Enum.reverse(issues))
|
|
|
|
~H"""
|
|
<div
|
|
:if={@issues != []}
|
|
class="border-l-4 border-yellow-500 bg-yellow-50 p-4 text-yellow-900 shadow-sm dark:bg-yellow-900/20 dark:text-yellow-300"
|
|
>
|
|
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />
|
|
<div>
|
|
<h3 class="font-bold">Issues Detected</h3>
|
|
<ul class="text-sm mt-1">
|
|
<li :for={issue <- @issues}>• {issue}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr :label, :string, required: true
|
|
attr :value, :string, required: true
|
|
attr :quality, :atom, default: :unknown
|
|
|
|
defp metric_card(assigns) do
|
|
color =
|
|
case assigns.quality do
|
|
:good -> "border-green-200 dark:border-green-800"
|
|
:warning -> "border-yellow-200 dark:border-yellow-800"
|
|
:bad -> "border-red-200 dark:border-red-800"
|
|
_ -> "border-gray-200 dark:border-gray-700"
|
|
end
|
|
|
|
assigns = assign(assigns, :color, color)
|
|
|
|
~H"""
|
|
<div class={"flex flex-col bg-gray-50 dark:bg-gray-900 rounded-xl border #{@color} p-4"}>
|
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">{@label}</div>
|
|
<div class="text-2xl font-bold">{@value}</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, t("Trace"))
|
|
|> assign(:query, "")
|
|
|> assign(:results, [])
|
|
|> assign(:trace, nil)
|
|
|> assign(:trace_type, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
organization = socket.assigns.current_scope.organization
|
|
|
|
socket =
|
|
case params do
|
|
%{"type" => type, "id" => id} ->
|
|
type_atom = safe_type(type)
|
|
trace = Trace.assemble_trace(organization.id, type_atom, id)
|
|
|
|
socket
|
|
|> assign(:trace, trace)
|
|
|> assign(:trace_type, type_atom)
|
|
|
|
_ ->
|
|
socket
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("search", %{"query" => query}, socket) do
|
|
organization = socket.assigns.current_scope.organization
|
|
|
|
results =
|
|
if String.trim(query) == "" do
|
|
[]
|
|
else
|
|
Trace.search(organization.id, query)
|
|
end
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:query, query)
|
|
|> assign(:results, results)
|
|
|> assign(:trace, nil)
|
|
|> assign(:trace_type, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("select_result", %{"type" => type, "id" => id}, socket) do
|
|
{:noreply, push_patch(socket, to: ~p"/trace?type=#{type}&id=#{id}")}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("clear", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> assign(:query, "")
|
|
|> assign(:results, [])
|
|
|> assign(:trace, nil)
|
|
|> assign(:trace_type, nil)
|
|
|> push_patch(to: ~p"/trace")}
|
|
end
|
|
|
|
def type_badge_class(:account), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
|
|
|
|
def type_badge_class(:inventory_item), do: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
|
|
def type_badge_class(:site), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
|
def type_badge_class(:device), do: "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400"
|
|
def type_badge_class(:access_point), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
|
def type_badge_class(_), do: "text-gray-700 dark:text-gray-300"
|
|
|
|
def type_badge_label(:account), do: "Account"
|
|
def type_badge_label(:inventory_item), do: "Inventory"
|
|
def type_badge_label(:site), do: "Site"
|
|
def type_badge_label(:device), do: "Device"
|
|
def type_badge_label(:access_point), do: "AP"
|
|
def type_badge_label(_), do: "Unknown"
|
|
|
|
@doc false
|
|
def status_badge_class("active"), do: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
|
|
def status_badge_class("suspended"), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
|
|
|
def status_badge_class("cancelled"), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
|
|
|
|
def status_badge_class(_), do: "text-gray-700 dark:text-gray-300"
|
|
|
|
@doc false
|
|
def device_status_color(:up), do: "text-green-600 dark:text-green-400"
|
|
def device_status_color(:down), do: "text-red-600 dark:text-red-400"
|
|
def device_status_color(_), do: "text-gray-500 dark:text-gray-400"
|
|
|
|
@doc false
|
|
def format_speed(nil), do: "—"
|
|
def format_speed(speed) when is_number(speed) and speed >= 1000, do: "#{Float.round(speed / 1000, 1)} Gbps"
|
|
def format_speed(speed) when is_number(speed), do: "#{speed} Mbps"
|
|
def format_speed(speed), do: "#{speed}"
|
|
|
|
@doc false
|
|
def format_relative_time(nil), do: "—"
|
|
|
|
def format_relative_time(dt) do
|
|
diff = DateTime.diff(DateTime.utc_now(), dt, :second)
|
|
format_relative_diff(diff)
|
|
end
|
|
|
|
defp format_relative_diff(diff) when diff < 60, do: "just now"
|
|
defp format_relative_diff(diff) when diff < 3600, do: "#{div(diff, 60)}m ago"
|
|
defp format_relative_diff(diff) when diff < 86_400, do: "#{div(diff, 3600)}h ago"
|
|
defp format_relative_diff(diff), do: "#{div(diff, 86_400)}d ago"
|
|
|
|
@doc false
|
|
def format_score(nil), do: "—"
|
|
def format_score(score) when is_number(score), do: "#{Float.round(score * 1.0, 1)}"
|
|
def format_score(score), do: "#{score}"
|
|
|
|
@doc false
|
|
def format_pct(nil), do: "—"
|
|
def format_pct(val) when is_number(val), do: "#{Float.round(val * 1.0, 1)}%"
|
|
def format_pct(val), do: "#{val}"
|
|
|
|
@doc false
|
|
def format_ms(nil), do: "—"
|
|
def format_ms(val) when is_number(val), do: "#{Float.round(val * 1.0, 1)} ms"
|
|
def format_ms(val), do: "#{val}"
|
|
|
|
@doc false
|
|
def format_throughput(nil), do: "—"
|
|
def format_throughput(val) when is_number(val) and val >= 1000, do: "#{Float.round(val / 1000, 1)} Gbps"
|
|
def format_throughput(val) when is_number(val), do: "#{Float.round(val * 1.0, 1)} Mbps"
|
|
def format_throughput(val), do: "#{val}"
|
|
|
|
@doc false
|
|
def format_alert_type(nil), do: "—"
|
|
def format_alert_type(type) when is_binary(type), do: type |> String.replace("_", " ") |> String.capitalize()
|
|
def format_alert_type(type), do: "#{type}"
|
|
|
|
@doc false
|
|
def score_quality(nil), do: :unknown
|
|
def score_quality(s) when s >= 80, do: :good
|
|
def score_quality(s) when s >= 50, do: :warning
|
|
def score_quality(_), do: :bad
|
|
|
|
@doc false
|
|
def score_color(nil), do: "text-gray-400"
|
|
def score_color(s) when s >= 80, do: "text-green-600 dark:text-green-400"
|
|
def score_color(s) when s >= 50, do: "text-yellow-600 dark:text-yellow-400"
|
|
def score_color(_), do: "text-red-600 dark:text-red-400"
|
|
|
|
@doc false
|
|
def latency_color(nil), do: "text-gray-400"
|
|
def latency_color(v) when v <= 30, do: "text-green-600 dark:text-green-400"
|
|
def latency_color(v) when v <= 80, do: "text-yellow-600 dark:text-yellow-400"
|
|
def latency_color(_), do: "text-red-600 dark:text-red-400"
|
|
|
|
@doc false
|
|
def jitter_color(nil), do: "text-gray-400"
|
|
def jitter_color(v) when v <= 10, do: "text-green-600 dark:text-green-400"
|
|
def jitter_color(v) when v <= 30, do: "text-yellow-600 dark:text-yellow-400"
|
|
def jitter_color(_), do: "text-red-600 dark:text-red-400"
|
|
|
|
@doc false
|
|
def loss_color(nil), do: "text-gray-400"
|
|
def loss_color(v) when v <= 1, do: "text-green-600 dark:text-green-400"
|
|
def loss_color(v) when v <= 5, do: "text-yellow-600 dark:text-yellow-400"
|
|
def loss_color(_), do: "text-red-600 dark:text-red-400"
|
|
|
|
@doc false
|
|
def device_status_dot(:up), do: "bg-green-500"
|
|
def device_status_dot(:down), do: "bg-red-500"
|
|
def device_status_dot(_), do: "bg-gray-400"
|
|
|
|
@doc false
|
|
def alert_dot(%{severity: "critical"}), do: "bg-red-500"
|
|
def alert_dot(%{severity: "warning"}), do: "bg-yellow-500"
|
|
def alert_dot(%{alert_type: "down"}), do: "bg-red-500"
|
|
def alert_dot(_), do: "bg-orange-400"
|
|
|
|
@doc false
|
|
def urgency_dot("critical"), do: "bg-red-500"
|
|
def urgency_dot("high"), do: "bg-orange-500"
|
|
def urgency_dot("medium"), do: "bg-yellow-500"
|
|
def urgency_dot(_), do: "bg-blue-400"
|
|
|
|
@doc false
|
|
def airtime_quality(nil), do: :unknown
|
|
def airtime_quality(v) when v <= 50, do: :good
|
|
def airtime_quality(v) when v <= 80, do: :warning
|
|
def airtime_quality(_), do: :bad
|
|
|
|
@doc false
|
|
def safe_type("account"), do: :account
|
|
def safe_type("inventory_item"), do: :inventory_item
|
|
def safe_type("site"), do: :site
|
|
def safe_type("device"), do: :device
|
|
def safe_type("access_point"), do: :access_point
|
|
def safe_type(_), do: :account
|
|
end
|