Collapse device page tabs into header row — single line nav

This commit is contained in:
Graham McIntire 2026-02-16 15:54:42 -06:00
parent 9dca0b099e
commit 6e3db720fd
5 changed files with 145 additions and 40 deletions

View file

@ -3,9 +3,9 @@
current_scope={@current_scope}
active_page="devices"
>
<%!-- Compact header --%>
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<%!-- Header: title + tabs + actions in one row --%>
<div class="flex items-center justify-between mb-4 border-b border-gray-200 dark:border-white/10 pb-3">
<div class="flex items-center gap-4">
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{@page_title}</h1>
<% percent =
if @device_quota.limit != :unlimited and @device_quota.limit > 0 do
@ -40,6 +40,41 @@
{@device_quota.current}/{@device_quota.limit}
<% end %>
</span>
<%!-- Inline tab switcher --%>
<nav class="flex items-center gap-1 ml-2">
<.link
patch={~p"/devices?tab=existing"}
class={[
"px-2.5 py-1 rounded-md text-sm font-medium transition-colors",
if @active_tab == "existing" do
"bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
else
"text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-800/50"
end
]}
>
{t("Existing")}
</.link>
<.link
patch={~p"/devices?tab=discovered"}
class={[
"inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-sm font-medium transition-colors",
if @active_tab == "discovered" do
"bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
else
"text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-800/50"
end
]}
>
{t("Discovered")}
<span
:if={@discovered_devices != []}
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
>
{length(@discovered_devices)}
</span>
</.link>
</nav>
</div>
<div class="flex items-center gap-2">
@ -85,43 +120,6 @@
</div>
</div>
<%!-- Tab Navigation --%>
<div class="border-b border-gray-200 dark:border-white/10 mb-4">
<nav class="-mb-px flex space-x-6">
<.link
patch={~p"/devices?tab=existing"}
class={[
"whitespace-nowrap py-2 px-1 border-b-2 text-sm font-medium",
if @active_tab == "existing" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400"
end
]}
>
{t("Existing Devices")}
</.link>
<.link
patch={~p"/devices?tab=discovered"}
class={[
"whitespace-nowrap py-2 px-1 border-b-2 text-sm font-medium",
if @active_tab == "discovered" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400"
end
]}
>
{t("Discovered")}
<%= if @discovered_devices != [] do %>
<span class="ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{length(@discovered_devices)}
</span>
<% end %>
</.link>
</nav>
</div>
<%= case @active_tab do %>
<% "existing" -> %>
<%= if @sites_enabled && !@has_sites && @has_devices do %>

View file

@ -265,6 +265,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|> assign_checks_data(device_id)
|> assign_preseem_data()
|> assign_gaiia_data()
|> assign_subscriber_impact()
end
# -- Selective reload helpers (used by handle_info handlers) --
@ -532,6 +533,20 @@ defmodule ToweropsWeb.DeviceLive.Show do
|> assign(:gaiia_network_site, gaiia_network_site)
end
# Subscriber impact data (from Gaiia device-to-subscriber matching).
defp assign_subscriber_impact(socket) do
device = socket.assigns.device
impact =
try do
Gaiia.get_device_impact(device.id)
rescue
_ -> %{subscriber_count: 0, mrr: nil, accounts: []}
end
assign(socket, :subscriber_impact, impact)
end
# Preseem tab data.
defp assign_preseem_data(socket) do
device = socket.assigns.device
@ -1622,6 +1637,15 @@ defmodule ToweropsWeb.DeviceLive.Show do
TimeHelpers.format_time_ago(datetime)
end
defp format_mrr(nil), do: "$0"
defp format_mrr(%Decimal{} = d) do
"$#{Decimal.round(d, 2) |> Decimal.to_string(:normal)}"
end
defp format_mrr(n) when is_number(n), do: "$#{:erlang.float_to_binary(n / 1, decimals: 2)}"
defp format_mrr(_), do: "$0"
defp config_change_dot_color(event) do
cond do
event.change_size > 50 -> "bg-red-500"

View file

@ -98,6 +98,28 @@
</.button>
</div>
</div>
<!-- Subscriber Impact Card -->
<%= if @subscriber_impact && @subscriber_impact.subscriber_count > 0 do %>
<div class="mb-4 rounded-lg border border-indigo-200 bg-indigo-50 dark:border-indigo-800/50 dark:bg-indigo-950/30 px-4 py-3">
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<.icon name="hero-users" class="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
<span class="text-sm font-semibold text-indigo-900 dark:text-indigo-200">
{@subscriber_impact.subscriber_count} {if @subscriber_impact.subscriber_count == 1, do: "subscriber", else: "subscribers"}
</span>
</div>
<%= if @can_view_financials && @subscriber_impact.mrr do %>
<div class="flex items-center gap-2">
<.icon name="hero-currency-dollar" class="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
<span class="text-sm font-semibold text-indigo-900 dark:text-indigo-200">
{format_mrr(@subscriber_impact.mrr)}/mo
</span>
</div>
<% end %>
</div>
</div>
<% end %>
<!-- Tabs -->
<div class="border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8 overflow-x-auto">

View file

@ -6,6 +6,7 @@ defmodule ToweropsWeb.SiteLive.Show do
alias Towerops.ConfigChanges
alias Towerops.Dashboard
alias Towerops.Devices
alias Towerops.Gaiia
alias Towerops.Monitoring
alias Towerops.Preseem
alias Towerops.Sites
@ -42,11 +43,20 @@ defmodule ToweropsWeb.SiteLive.Show do
# Device response times for health grid
response_times = Monitoring.get_device_latest_response_times(device_ids)
# Subscriber impact per device from Gaiia
site_impact =
try do
Gaiia.get_site_impact(site.id)
rescue
_ -> %{subscriber_count: 0, mrr: nil, devices: []}
end
{:noreply,
socket
|> assign(:page_title, site.name)
|> assign(:site, site)
|> assign(:device, devices)
|> assign(:site_impact, site_impact)
|> assign(:latency_chart_data, latency_chart_data)
|> assign(:site_summary, site_summary)
|> assign(:qoe_summary, qoe_summary)

View file

@ -160,6 +160,57 @@
</div>
<% end %>
<%!-- Subscriber Impact Breakdown by Device --%>
<%= if @site_impact && @site_impact.subscriber_count > 0 do %>
<div class="mt-6 rounded-lg border border-indigo-200 bg-indigo-50 dark:border-indigo-800/50 dark:bg-indigo-950/30">
<div class="px-4 py-3 border-b border-indigo-200 dark:border-indigo-800/50">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-indigo-900 dark:text-indigo-200 flex items-center gap-1.5">
<.icon name="hero-users" class="h-4 w-4" /> {t("Subscriber Impact")}
</h3>
<div class="flex items-center gap-4 text-sm font-medium text-indigo-900 dark:text-indigo-200">
<span>{format_number(@site_impact.subscriber_count)} subscribers</span>
<%= if @can_view_financials && @site_impact.mrr do %>
<span>{format_mrr(@site_impact.mrr)}/mo</span>
<% end %>
</div>
</div>
</div>
<%= if @site_impact.devices != [] do %>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-indigo-200 dark:divide-indigo-800/50">
<thead>
<tr class="text-xs text-indigo-700 dark:text-indigo-300">
<th class="px-4 py-2 text-left font-medium">{t("Device")}</th>
<th class="px-4 py-2 text-right font-medium">{t("Subscribers")}</th>
<%= if @can_view_financials do %>
<th class="px-4 py-2 text-right font-medium">{t("MRR")}</th>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-indigo-100 dark:divide-indigo-900/30">
<%= for dev <- @site_impact.devices do %>
<tr class="text-sm">
<td class="px-4 py-2 text-indigo-900 dark:text-indigo-200 font-medium">
{dev.name || dev.device_id}
</td>
<td class="px-4 py-2 text-right text-indigo-800 dark:text-indigo-300 font-mono">
{dev.count}
</td>
<%= if @can_view_financials do %>
<td class="px-4 py-2 text-right text-indigo-800 dark:text-indigo-300 font-mono">
{format_mrr(dev.mrr)}
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
<% end %>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
<%!-- Left Column --%>
<div class="space-y-6">