706 lines
33 KiB
Text
706 lines
33 KiB
Text
<Layouts.authenticated
|
||
flash={@flash}
|
||
current_scope={@current_scope}
|
||
active_page="devices"
|
||
>
|
||
<%!-- Header: title + actions, then tabs below on mobile --%>
|
||
<div class="mb-4 border-b border-gray-200 dark:border-white/10 pb-3">
|
||
<div class="flex items-center justify-between gap-2">
|
||
<div class="flex items-center gap-2 min-w-0">
|
||
<h1 class="text-xl font-bold text-gray-900 dark:text-white truncate">{@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={"shrink-0 rounded px-2 py-0.5 text-xs font-medium #{badge_class}"}>
|
||
<%= cond do %>
|
||
<% @current_scope.user.is_superuser -> %>
|
||
{@device_quota.current}
|
||
<% @device_quota.limit == :unlimited -> %>
|
||
{@device_quota.current}
|
||
<% true -> %>
|
||
{@device_quota.current}/{@device_quota.limit}
|
||
<% end %>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 shrink-0">
|
||
<%= 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" />
|
||
<span class="hidden sm:inline">{t("Reset")}</span>
|
||
</.button>
|
||
<.button type="button" phx-click="toggle_reorder_mode" variant="primary">
|
||
<.icon name="hero-check" class="h-4 w-4" />
|
||
<span class="hidden sm:inline">{t("Done")}</span>
|
||
</.button>
|
||
<% else %>
|
||
<.button
|
||
type="button"
|
||
phx-click="toggle_reorder_mode"
|
||
title="Reorder devices and sites"
|
||
>
|
||
<.icon name="hero-bars-arrow-up" 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" />
|
||
<span class="hidden sm:inline">{t("New Device")}</span>
|
||
<span class="sm:hidden">{t("New")}</span>
|
||
</.button>
|
||
<%= if @has_devices and not @reorder_mode do %>
|
||
<.button
|
||
type="button"
|
||
phx-click="force_rediscover_all"
|
||
title="Rediscover all devices"
|
||
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>
|
||
<%!-- Tabs on second line --%>
|
||
<nav class="flex items-center gap-1 mt-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={@pagination && @pagination.total_count > 0}
|
||
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"
|
||
>
|
||
{@pagination.total_count}
|
||
</span>
|
||
</.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-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:-ml-4 lg:-mr-8 lg:pl-4 lg:pr-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="hidden sm:table-cell 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="hidden md:table-cell 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="hidden lg:table-cell 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="hidden lg:table-cell 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",
|
||
"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",
|
||
"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="hidden sm:table-cell 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",
|
||
"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",
|
||
"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="hidden md:table-cell p-0 whitespace-nowrap">
|
||
<.link
|
||
navigate={~p"/devices/#{row.device.id}"}
|
||
class={[
|
||
"block px-3 text-inherit no-underline",
|
||
"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="hidden lg:table-cell p-0 whitespace-nowrap">
|
||
<.link
|
||
navigate={~p"/devices/#{row.device.id}"}
|
||
class={[
|
||
"block px-3 text-inherit no-underline",
|
||
"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="hidden lg:table-cell p-0 whitespace-nowrap">
|
||
<.link
|
||
navigate={~p"/devices/#{row.device.id}"}
|
||
class={[
|
||
"block px-3 text-inherit no-underline",
|
||
"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="hero-server"
|
||
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>
|