Collapse device page tabs into header row — single line nav
This commit is contained in:
parent
9dca0b099e
commit
6e3db720fd
5 changed files with 145 additions and 40 deletions
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue