Transform the dashboard into a single-pane-of-glass command center that blends operational metrics with business context from Gaiia subscriber data. - Extend insight schema with multi-source support (snmp, gaiia, system) - Add Oban workers for automated insight generation (device health, system, gaiia) - New Dashboard context with health score computation and site summaries - Rewrite dashboard LiveView with health overview, alert/insight feeds, site grid - Add source filter pills for insights with URL state management - Add metrics bar to site detail pages (device count, alerts, subscribers, MRR) - Add subscriber/MRR context to alert list site links - Add subscriber badges to device list site headers - Real-time PubSub updates for alerts on dashboard
597 lines
26 KiB
Text
597 lines
26 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="devices"
|
|
>
|
|
<div class="flex items-start justify-between mb-8">
|
|
<div>
|
|
<.header>
|
|
{@page_title}
|
|
<:subtitle>Monitor and manage all your network devices</:subtitle>
|
|
</.header>
|
|
</div>
|
|
|
|
<% 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-800 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-800 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800"
|
|
|
|
percent >= 90 ->
|
|
"bg-orange-50 text-orange-800 border-orange-200 dark:bg-orange-900/20 dark:text-orange-400 dark:border-orange-800"
|
|
|
|
percent >= 75 ->
|
|
"bg-yellow-50 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800"
|
|
|
|
@device_quota.limit == :unlimited ->
|
|
"bg-blue-50 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
|
|
|
|
true ->
|
|
"bg-green-50 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800"
|
|
end %>
|
|
|
|
<div class={"rounded-lg px-3 py-2 text-sm font-medium border #{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} devices
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 mb-6">
|
|
<%= if @has_devices do %>
|
|
<%= if @reorder_mode do %>
|
|
<.button
|
|
type="button"
|
|
phx-click="reset_order"
|
|
data-confirm="Reset all sites and devices to alphabetical order?"
|
|
>
|
|
<.icon name="hero-arrow-path" class="h-4 w-4" /> Reset Order
|
|
</.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" /> Reorder
|
|
</.button>
|
|
<% end %>
|
|
<% end %>
|
|
|
|
<.button
|
|
:if={!@sites_enabled || @has_sites}
|
|
navigate={~p"/devices/new"}
|
|
variant="primary"
|
|
>
|
|
<.icon name="hero-plus" class="h-5 w-5" /> New Device
|
|
</.button>
|
|
|
|
<%= if @has_devices and not @reorder_mode do %>
|
|
<.button
|
|
type="button"
|
|
phx-click="force_rediscover_all"
|
|
data-confirm="This will trigger SNMP discovery for all SNMP-enabled devices. Continue?"
|
|
>
|
|
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Force Rediscover All
|
|
</.button>
|
|
<% end %>
|
|
</div>
|
|
|
|
<!-- Tab Navigation -->
|
|
<div class="border-b border-gray-200 dark:border-white/10 mb-6">
|
|
<nav class="-mb-px flex space-x-8">
|
|
<.link
|
|
patch={~p"/devices?tab=existing"}
|
|
class={[
|
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
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 dark:hover:text-gray-300"
|
|
end
|
|
]}
|
|
>
|
|
Existing Devices
|
|
</.link>
|
|
|
|
<.link
|
|
patch={~p"/devices?tab=discovered"}
|
|
class={[
|
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
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 dark:hover:text-gray-300"
|
|
end
|
|
]}
|
|
>
|
|
Discovered Devices
|
|
<%= if @discovered_devices != [] do %>
|
|
<span class="ml-2 inline-flex items-center px-2 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 %>
|
|
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-4 mb-6">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<.icon name="hero-information-circle" class="h-5 w-5 text-blue-400" />
|
|
</div>
|
|
<div class="ml-3 flex-1">
|
|
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
|
|
Organize your devices with sites
|
|
</h3>
|
|
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
|
<p>
|
|
Sites help you organize devices by physical location. Create a site to assign your devices and keep things organized.
|
|
</p>
|
|
</div>
|
|
<div class="mt-4">
|
|
<.button navigate={~p"/sites/new"}>
|
|
<.icon name="hero-plus" class="h-4 w-4" /> Create Your First Site
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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" />
|
|
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">No devices</h3>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
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" /> New Device
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="mt-8 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-white dark:bg-gray-900">
|
|
<tr>
|
|
<th
|
|
:if={@reorder_mode}
|
|
scope="col"
|
|
class="py-3.5 pl-4 pr-2 w-8 sm:pl-3"
|
|
>
|
|
<span class="sr-only">Drag handle</span>
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class={[
|
|
"py-3.5 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3 dark:text-white",
|
|
@reorder_mode && "pl-2",
|
|
!@reorder_mode && "pl-4"
|
|
]}
|
|
>
|
|
Name
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
|
|
>
|
|
IP Address
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
|
|
>
|
|
Status
|
|
</th>
|
|
<th
|
|
scope="col"
|
|
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
|
|
>
|
|
Last Checked
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="device-rows" class="bg-white dark:bg-gray-900" phx-update="stream">
|
|
<%= for {dom_id, row} <- @streams.device_rows do %>
|
|
<%= if row.type == :site_header do %>
|
|
<tr
|
|
id={dom_id}
|
|
class={[
|
|
"border-t site-header border-gray-200",
|
|
"dark:border-white/10"
|
|
]}
|
|
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-2 pl-4 pr-2 sm:pl-3 bg-gray-50 dark:bg-gray-800/50"
|
|
>
|
|
<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-5 w-5" />
|
|
</button>
|
|
</td>
|
|
<th
|
|
scope="colgroup"
|
|
colspan="4"
|
|
class={[
|
|
"bg-gray-50 py-2 pr-3 text-left dark:bg-gray-800/50",
|
|
!@reorder_mode && "pl-4 sm:pl-3"
|
|
]}
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<%= if row.site do %>
|
|
<.link
|
|
navigate={~p"/sites/#{row.site.id}"}
|
|
class="text-sm font-semibold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400"
|
|
>
|
|
{row.site.name}
|
|
</.link>
|
|
<% else %>
|
|
<span class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{if @sites_enabled,
|
|
do: "Unassigned Devices",
|
|
else: "All Devices"}
|
|
</span>
|
|
<% end %>
|
|
<span class="text-gray-400 dark:text-gray-600">·</span>
|
|
<span class="text-xs text-gray-600 dark:text-gray-400">
|
|
{row.stats.total} {if row.stats.total == 1,
|
|
do: "device",
|
|
else: "devices"}
|
|
</span>
|
|
<span
|
|
:if={row.stats.up > 0}
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
|
>
|
|
{row.stats.up} up
|
|
</span>
|
|
<span
|
|
:if={row.stats.down > 0}
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
|
>
|
|
{row.stats.down} down
|
|
</span>
|
|
<%= if row.site && @site_subscribers[row.site.id] do %>
|
|
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-amber-50 text-amber-800 dark:bg-amber-400/10 dark:text-amber-400">
|
|
<.icon name="hero-users" class="h-3 w-3" />
|
|
{@site_subscribers[row.site.id].account_count} subscribers
|
|
</span>
|
|
<% end %>
|
|
</div>
|
|
<p
|
|
:if={row.site && row.site.location}
|
|
class="text-xs text-gray-500 dark:text-gray-400"
|
|
>
|
|
<.icon name="hero-map-pin" class="inline h-3 w-3" /> {row.site.location}
|
|
</p>
|
|
</div>
|
|
</th>
|
|
</tr>
|
|
<% else %>
|
|
<tr
|
|
id={dom_id}
|
|
class={[
|
|
"border-t device-row hover:bg-gray-50 dark:hover:bg-gray-800/50",
|
|
if(row.device_index == 0,
|
|
do: "border-gray-300 dark:border-white/15",
|
|
else: "border-gray-200 dark:border-white/10"
|
|
)
|
|
]}
|
|
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="py-4 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 device"
|
|
>
|
|
<.icon name="hero-bars-3" class="h-5 w-5" />
|
|
</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={[
|
|
"block py-4 pr-3 text-inherit no-underline",
|
|
@reorder_mode && "pl-2",
|
|
!@reorder_mode && "pl-4 sm:pl-3"
|
|
]}
|
|
>
|
|
{row.device.name}
|
|
</.link>
|
|
</td>
|
|
<td class="p-0 text-sm whitespace-nowrap text-gray-500 dark:text-gray-400 font-mono">
|
|
<.link
|
|
navigate={~p"/devices/#{row.device.id}"}
|
|
class="block px-3 py-4 text-inherit no-underline"
|
|
>
|
|
{row.device.ip_address}
|
|
</.link>
|
|
</td>
|
|
<td class="p-0 text-sm whitespace-nowrap">
|
|
<.link
|
|
navigate={~p"/devices/#{row.device.id}"}
|
|
class="block px-3 py-4 text-inherit no-underline"
|
|
>
|
|
<span class={[
|
|
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
|
row.device.status == :up &&
|
|
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
row.device.status == :down &&
|
|
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
|
row.device.status == :unknown &&
|
|
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
|
]}>
|
|
{row.device.status |> to_string() |> String.upcase()}
|
|
</span>
|
|
</.link>
|
|
</td>
|
|
<td class="p-0 text-sm whitespace-nowrap text-gray-500 dark:text-gray-400">
|
|
<.link
|
|
navigate={~p"/devices/#{row.device.id}"}
|
|
class="block px-3 py-4 text-inherit no-underline"
|
|
>
|
|
<.timestamp
|
|
datetime={row.device.last_checked_at}
|
|
timezone={@timezone}
|
|
/>
|
|
</.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"
|
|
/>
|
|
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
|
|
No discovered devices
|
|
</h3>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
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="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">Unknown</span>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={discovered} label="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="Type">
|
|
<div class="flex items-center gap-2">
|
|
<.icon
|
|
name={device_type_icon(discovered.device_type)}
|
|
class="h-4 w-4 text-gray-500"
|
|
/>
|
|
<span class="text-sm text-gray-900 dark:text-white">
|
|
{device_type_label(discovered.device_type)}
|
|
</span>
|
|
</div>
|
|
</:col>
|
|
|
|
<:col :let={discovered} label="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="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"> ({first.interface})</span>
|
|
<% else %>
|
|
<span>{length(discovered.discovered_by)} devices</span>
|
|
<% end %>
|
|
</div>
|
|
</:col>
|
|
|
|
<:col :let={discovered} label="Last Seen">
|
|
<.timestamp
|
|
datetime={discovered.last_seen}
|
|
class="text-sm text-gray-600 dark:text-gray-400"
|
|
/>
|
|
</:col>
|
|
|
|
<:col :let={discovered} label="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 Device
|
|
</.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"
|
|
>
|
|
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"
|
|
>
|
|
Next
|
|
</.link>
|
|
</div>
|
|
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-700 dark:text-gray-400">
|
|
Showing
|
|
<span class="font-medium">
|
|
{(@pagination.page - 1) * @pagination.per_page + 1}
|
|
</span>
|
|
to
|
|
<span class="font-medium">
|
|
{min(@pagination.page * @pagination.per_page, @pagination.total_count)}
|
|
</span>
|
|
of <span class="font-medium">{@pagination.total_count}</span>
|
|
results
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<nav
|
|
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
|
|
aria-label="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"
|
|
>
|
|
<span class="sr-only">Previous</span>
|
|
<.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 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
|
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"
|
|
>
|
|
<span class="sr-only">Next</span>
|
|
<.icon name="hero-chevron-right" class="h-5 w-5" />
|
|
</.link>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|
|
</Layouts.authenticated>
|