towerops/lib/towerops_web/live/device_live/index.html.heex

724 lines
34 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="devices"
>
<%!-- 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
(@device_quota.current / @device_quota.limit * 100) |> trunc()
else
0
end %>
<% badge_class =
cond do
@current_scope.user.is_superuser ->
"bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
@device_quota.limit != :unlimited and @device_quota.current >= @device_quota.limit ->
"bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400"
percent >= 90 ->
"bg-orange-50 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400"
@device_quota.limit == :unlimited ->
"bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
true ->
"bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400"
end %>
<span class={"rounded px-2 py-0.5 text-xs font-medium #{badge_class}"}>
<%= cond do %>
<% @current_scope.user.is_superuser -> %>
{@device_quota.current} devices
<% @device_quota.limit == :unlimited -> %>
{@device_quota.current} devices
<% true -> %>
{@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">
<%= if @has_devices do %>
<%= if @reorder_mode do %>
<.button
type="button"
phx-click="reset_order"
data-confirm={t("Reset all sites and devices to alphabetical order?")}
>
<.icon name="hero-arrow-path" class="h-4 w-4" /> Reset
</.button>
<.button type="button" phx-click="toggle_reorder_mode" variant="primary">
<.icon name="hero-check" class="h-4 w-4" /> Done
</.button>
<% else %>
<.button type="button" phx-click="toggle_reorder_mode">
<.icon name="hero-bars-3" class="h-4 w-4" />
</.button>
<.button
type="button"
phx-click="toggle_compact_mode"
variant={if @compact_mode, do: "primary", else: nil}
>
<.icon name="hero-bars-2" class="h-4 w-4" />
</.button>
<% end %>
<% end %>
<.button :if={!@sites_enabled || @has_sites} navigate={~p"/devices/new"} variant="primary">
<.icon name="hero-plus" class="h-4 w-4" /> {t("New Device")}
</.button>
<%= if @has_devices and not @reorder_mode do %>
<.button
type="button"
phx-click="force_rediscover_all"
data-confirm={
t("This will trigger SNMP discovery for all SNMP-enabled devices. Continue?")
}
>
<.icon name="hero-magnifying-glass" class="h-4 w-4" />
</.button>
<% end %>
</div>
</div>
<%= case @active_tab do %>
<% "existing" -> %>
<%= if @sites_enabled && !@has_sites && @has_devices do %>
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 mb-4 flex items-center gap-3">
<.icon name="hero-information-circle" class="h-5 w-5 text-blue-400 flex-shrink-0" />
<p class="text-sm text-blue-800 dark:text-blue-200 flex-1">
{t("Organize your devices with sites")} —
<.link navigate={~p"/sites/new"} class="font-medium underline">
{t("Create a site")}
</.link>
</p>
</div>
<% end %>
<%= if !@has_devices do %>
<div class="text-center py-16">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 dark:text-gray-400" />
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No devices")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Get started by adding your first device.")}
</p>
<div class="mt-6">
<.button navigate={~p"/devices/new"} variant="primary">
<.icon name="hero-plus" class="h-5 w-5" /> {t("New Device")}
</.button>
</div>
</div>
<% else %>
<%!-- Sticky Status Bar + Search --%>
<div class="sticky top-0 z-10 bg-white dark:bg-gray-900 pb-3 -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8 border-b border-gray-100 dark:border-white/5">
<div class="flex items-center gap-3 pt-2">
<%!-- Search --%>
<form phx-change="search" class="flex-1 max-w-sm">
<div class="relative">
<.icon
name="hero-magnifying-glass"
class="absolute left-2.5 top-2 h-4 w-4 text-gray-400"
/>
<input
type="text"
name="search_query"
value={@search_query}
placeholder={t("Filter by name or IP...")}
phx-debounce="150"
class="block w-full rounded-md border-0 py-1.5 pl-9 pr-3 text-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-white/10 placeholder:text-gray-400 focus:ring-2 focus:ring-blue-500 font-mono"
autocomplete="off"
spellcheck="false"
/>
</div>
</form>
<%!-- Status summary + filter buttons --%>
<div class="flex items-center gap-2 text-xs font-medium">
<span class="text-gray-500 dark:text-gray-400 font-mono">{@total_devices}</span>
<button
type="button"
phx-click="filter_status"
phx-value-status="up"
class={[
"inline-flex items-center gap-1 px-2 py-1 rounded-md transition-colors",
@status_filter == "up" &&
"bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 ring-1 ring-green-300 dark:ring-green-700",
@status_filter != "up" &&
"text-green-700 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/20"
]}
>
<span class="inline-block h-2 w-2 rounded-full bg-green-500"></span>
{@total_up}
</button>
<button
:if={@total_down > 0}
type="button"
phx-click="filter_status"
phx-value-status="down"
class={[
"inline-flex items-center gap-1 px-2 py-1 rounded-md transition-colors",
@status_filter == "down" &&
"bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300 ring-1 ring-red-300 dark:ring-red-700",
@status_filter != "down" &&
"text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
]}
>
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
{@total_down}
</button>
<button
:if={@total_unknown > 0}
type="button"
phx-click="filter_status"
phx-value-status="unknown"
class={[
"inline-flex items-center gap-1 px-2 py-1 rounded-md transition-colors",
@status_filter == "unknown" &&
"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 ring-1 ring-gray-400 dark:ring-gray-600",
@status_filter != "unknown" &&
"text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
]}
>
<span class="inline-block h-2 w-2 rounded-full bg-gray-400"></span>
{@total_unknown}
</button>
<button
:if={@status_filter != "all"}
type="button"
phx-click="filter_status"
phx-value-status="all"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-1"
title={t("Clear filter")}
>
<.icon name="hero-x-mark" class="h-4 w-4" />
</button>
</div>
</div>
</div>
<%!-- Device Table --%>
<div class="mt-2 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="relative min-w-full" phx-hook="DeviceListReorder" id="device-list">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th :if={@reorder_mode} scope="col" class="py-2 pl-4 pr-2 w-8 sm:pl-3">
<span class="sr-only">Drag</span>
</th>
<th
scope="col"
class={[
"py-2 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400",
@reorder_mode && "pl-2",
!@reorder_mode && "pl-4 sm:pl-3"
]}
>
{t("Device")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("IP Address")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Status")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Type")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Last Seen")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Response")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Subs")}
</th>
</tr>
</thead>
<tbody
id="device-rows"
class="divide-y divide-gray-100 dark:divide-white/5"
phx-update="stream"
>
<%= for {dom_id, row} <- @streams.device_rows do %>
<%= if row.type == :site_header do %>
<tr
id={dom_id}
class="bg-gray-50/80 dark:bg-gray-800/30"
data-site-id={if row.site, do: row.site.id, else: "no-site"}
draggable={if @reorder_mode && row.site, do: "true", else: "false"}
>
<td :if={@reorder_mode && row.site} class="py-1.5 pl-4 pr-2 sm:pl-3">
<button
type="button"
class="drag-handle cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title="Drag to reorder site"
>
<.icon name="hero-bars-3" class="h-4 w-4" />
</button>
</td>
<th
scope="colgroup"
colspan={if @reorder_mode, do: "7", else: "7"}
class={["py-1.5 pr-3", !@reorder_mode && "pl-4 sm:pl-3"]}
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs">
<%= if row.site do %>
<.link
navigate={~p"/sites/#{row.site.id}"}
class="font-semibold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400"
>
{row.site.name}
</.link>
<% else %>
<span class="font-semibold text-gray-700 dark:text-gray-300">
{if @sites_enabled, do: "Unassigned", else: "All Devices"}
</span>
<% end %>
<span class="text-gray-400 dark:text-gray-600">·</span>
<span class="text-gray-500 dark:text-gray-400 font-mono">
{row.stats.total}
</span>
<span
:if={row.stats.up > 0}
class="text-green-600 dark:text-green-400"
>
<span class="inline-block h-1.5 w-1.5 rounded-full bg-green-500">
</span>
{row.stats.up}
</span>
<span
:if={row.stats.down > 0}
class="text-red-600 dark:text-red-400 font-medium"
>
<span class="inline-block h-1.5 w-1.5 rounded-full bg-red-500">
</span>
{row.stats.down}
</span>
<%= if row.site && @site_subscribers[row.site.id] do %>
<span class="text-amber-600 dark:text-amber-400">
<.icon name="hero-users" class="inline h-3 w-3" />
{@site_subscribers[row.site.id].account_count}
</span>
<% end %>
</div>
<span
:if={row.site && row.site.location}
class="text-xs text-gray-400 dark:text-gray-500 dark:text-gray-400"
>
<.icon name="hero-map-pin" class="inline h-3 w-3" /> {row.site.location}
</span>
</div>
</th>
</tr>
<% else %>
<tr
id={dom_id}
class={[
"device-row hover:bg-blue-50/50 dark:hover:bg-blue-900/10 transition-colors cursor-pointer",
rem(row.device_index, 2) == 1 && "bg-gray-50/40 dark:bg-gray-800/20"
]}
title={device_row_title(row.device)}
data-device-id={row.device.id}
data-site-id={if row.site, do: row.site.id, else: "no-site"}
data-device-position={row.device_index + 1}
draggable={if @reorder_mode, do: "true", else: "false"}
>
<td
:if={@reorder_mode}
class={[
"pl-4 pr-2 sm:pl-3",
if(@compact_mode, do: "py-1", else: "py-2")
]}
>
<button
type="button"
class="drag-handle cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title="Drag to reorder device"
>
<.icon name="hero-bars-3" class="h-4 w-4" />
</button>
</td>
<td class="p-0 text-sm font-medium whitespace-nowrap text-gray-900 dark:text-white">
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
"flex items-center gap-2 pr-3 text-inherit no-underline",
@reorder_mode && "pl-2",
!@reorder_mode && "pl-4 sm:pl-3",
if(@compact_mode, do: "py-1", else: "py-2")
]}
>
<span
class={"inline-block h-2 w-2 rounded-full flex-shrink-0 #{health_dot_color(row.health)}"}
title={health_dot_title(row.health)}
/>
{row.device.name}
</.link>
</td>
<td class="p-0 whitespace-nowrap">
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
"block px-3 text-inherit no-underline font-mono text-xs text-gray-600 dark:text-gray-300",
if(@compact_mode, do: "py-1", else: "py-2")
]}
>
{row.device.ip_address}
</.link>
</td>
<td class="p-0 whitespace-nowrap">
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
"block px-3 text-inherit no-underline",
if(@compact_mode, do: "py-1", else: "py-2")
]}
>
<span class="inline-flex items-center gap-1">
<span class={[
"inline-block h-2 w-2 rounded-full",
row.device.status == :up && "bg-green-500",
row.device.status == :down && "bg-red-500",
row.device.status == :unknown && "bg-gray-400"
]} />
<span class={[
"text-xs font-mono font-medium",
row.device.status == :up && "text-green-700 dark:text-green-400",
row.device.status == :down && "text-red-700 dark:text-red-400",
row.device.status == :unknown &&
"text-gray-500 dark:text-gray-400"
]}>
{row.device.status |> to_string() |> String.upcase()}
</span>
</span>
</.link>
</td>
<td class="p-0 whitespace-nowrap">
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
"flex items-center gap-1.5 px-3 text-inherit no-underline",
if(@compact_mode, do: "py-1", else: "py-2")
]}
>
<.icon
name={device_type_icon(row.device.device_role)}
class="h-3.5 w-3.5 text-gray-400 dark:text-gray-500 dark:text-gray-400 flex-shrink-0"
/>
<span class="text-xs text-gray-600 dark:text-gray-400">
{(row.device.device_role || :unknown)
|> to_string()
|> String.capitalize()}
</span>
</.link>
</td>
<td class="p-0 whitespace-nowrap">
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
"block px-3 text-inherit no-underline",
if(@compact_mode, do: "py-1", else: "py-2")
]}
>
<% {text, color} = last_seen_text(row.device.last_checked_at) %>
<span class={"text-xs font-mono #{color}"}>{text}</span>
</.link>
</td>
<td class="p-0 whitespace-nowrap">
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
"block px-3 text-inherit no-underline",
if(@compact_mode, do: "py-1", else: "py-2")
]}
>
<div class="flex items-center gap-2">
<% {rt_text, rt_color} = response_time_badge(row.response) %>
<span class={"text-xs font-mono #{rt_color}"}>{rt_text}</span>
<% bar_w = response_time_bar_width(row.response) %>
<div
:if={bar_w > 0}
class="w-16 h-1.5 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden"
>
<div
class={"h-full rounded-full #{response_time_bar_color(row.response)}"}
style={"width: #{bar_w}%"}
/>
</div>
</div>
</.link>
</td>
<td class="p-0 whitespace-nowrap">
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
"block px-3 text-inherit no-underline",
if(@compact_mode, do: "py-1", else: "py-2")
]}
>
<%= if sub_text = format_subscriber_count(row.subscribers) do %>
<span class="inline-flex items-center gap-1 text-xs text-blue-700 dark:text-blue-300">
<.icon name="hero-users" class="h-3 w-3" />
{sub_text}
</span>
<% else %>
<span class="text-xs text-gray-300 dark:text-gray-600">—</span>
<% end %>
</.link>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<% end %>
<% "discovered" -> %>
<%= if @discovered_devices == [] do %>
<div class="text-center py-16">
<.icon
name="hero-magnifying-glass"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 dark:text-gray-400"
/>
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No discovered devices")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Devices will appear here as they are discovered via LLDP/CDP.")}
</p>
</div>
<% else %>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Showing {(@pagination.page - 1) * @pagination.per_page + 1}-{min(
@pagination.page * @pagination.per_page,
@pagination.total_count
)} of {@pagination.total_count} discovered devices
</div>
<.table id="discovered-devices" rows={@discovered_devices}>
<:col :let={discovered} label={t("Identifier")}>
<%= case discovered.identifier.type do %>
<% :mac -> %>
<div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
MAC
</span>
<span class="ml-2 font-mono text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
</span>
</div>
<% :ip -> %>
<div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
IP
</span>
<span class="ml-2 font-mono text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
</span>
</div>
<% :hostname -> %>
<div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
HOST
</span>
<span class="ml-2 text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
</span>
</div>
<% _ -> %>
<span class="text-sm text-gray-500 dark:text-gray-400">Unknown</span>
<% end %>
</:col>
<:col :let={discovered} label={t("Hostname")}>
<%= if discovered.hostname do %>
<span class="text-sm font-medium text-gray-900 dark:text-white">
{discovered.hostname}
</span>
<% else %>
<span class="text-sm text-gray-400">-</span>
<% end %>
</:col>
<:col :let={discovered} label={t("Type")}>
<div class="flex items-center gap-2">
<.icon
name={device_type_icon(discovered.device_type)}
class="h-4 w-4 text-gray-500 dark:text-gray-400"
/>
<span class="text-sm text-gray-900 dark:text-white">
{device_type_label(discovered.device_type)}
</span>
</div>
</:col>
<:col :let={discovered} label={t("Manufacturer")}>
<%= if discovered.manufacturer do %>
<span class="text-sm text-gray-900 dark:text-white">{discovered.manufacturer}</span>
<% else %>
<span class="text-sm text-gray-400">-</span>
<% end %>
</:col>
<:col :let={discovered} label={t("Discovered By")}>
<div class="text-sm text-gray-600 dark:text-gray-400">
<%= if length(discovered.discovered_by) == 1 do %>
<% [first] = discovered.discovered_by %>
<.link
navigate={~p"/devices/#{first.device_id}"}
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{first.device_name}
</.link>
<span class="text-xs text-gray-500 dark:text-gray-400"> ({first.interface})</span>
<% else %>
<span>{length(discovered.discovered_by)} devices</span>
<% end %>
</div>
</:col>
<:col :let={discovered} label={t("Last Seen")}>
<.timestamp
datetime={discovered.last_seen}
class="text-sm text-gray-600 dark:text-gray-400"
/>
</:col>
<:col :let={discovered} label={t("Actions")}>
<.button
phx-click="add_discovered_device"
phx-value-identifier={Jason.encode!(discovered.identifier)}
variant="primary"
>
<.icon name="hero-plus" class="h-3 w-3" /> Add
</.button>
</:col>
</.table>
<%= if @pagination.total_pages > 1 do %>
<div class="mt-6 flex items-center justify-between border-t border-gray-200 dark:border-white/10 pt-6">
<div class="flex flex-1 justify-between sm:hidden">
<.link
:if={@pagination.page > 1}
patch={~p"/devices?tab=discovered&page=#{@pagination.page - 1}"}
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{t("Previous")}
</.link>
<.link
:if={@pagination.page < @pagination.total_pages}
patch={~p"/devices?tab=discovered&page=#{@pagination.page + 1}"}
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{t("Next")}
</.link>
</div>
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<p class="text-sm text-gray-700 dark:text-gray-400">
<span class="font-medium">{(@pagination.page - 1) * @pagination.per_page + 1}</span><span class="font-medium">{min(@pagination.page * @pagination.per_page, @pagination.total_count)}</span> of
<span class="font-medium">{@pagination.total_count}</span>
</p>
<nav
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
aria-label={t("Pagination")}
>
<.link
:if={@pagination.page > 1}
patch={~p"/devices?tab=discovered&page=#{@pagination.page - 1}"}
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<.icon name="hero-chevron-left" class="h-5 w-5" />
</.link>
<%= for page_num <- pagination_range(@pagination.page, @pagination.total_pages) do %>
<%= if page_num == :ellipsis do %>
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 dark:text-gray-400 dark:ring-white/10">
...
</span>
<% else %>
<.link
patch={~p"/devices?tab=discovered&page=#{page_num}"}
class={[
"relative inline-flex items-center px-4 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 dark:ring-white/10",
if @pagination.page == page_num do
"z-10 bg-blue-600 text-white"
else
"text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
end
]}
>
{page_num}
</.link>
<% end %>
<% end %>
<.link
:if={@pagination.page < @pagination.total_pages}
patch={~p"/devices?tab=discovered&page=#{@pagination.page + 1}"}
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<.icon name="hero-chevron-right" class="h-5 w-5" />
</.link>
</nav>
</div>
</div>
<% end %>
<% end %>
<% end %>
</Layouts.authenticated>