477 lines
20 KiB
Text
477 lines
20 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 @device != [] 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={@has_sites}
|
|
navigate={~p"/devices/new"}
|
|
variant="primary"
|
|
>
|
|
<.icon name="hero-plus" class="h-5 w-5" /> New Device
|
|
</.button>
|
|
|
|
<%= if @device != [] 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 !@has_sites do %>
|
|
<div class="text-center py-16">
|
|
<.icon
|
|
name="hero-building-office"
|
|
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">
|
|
Create a site first
|
|
</h3>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Before adding device, you need to create at least one site location.
|
|
</p>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
Sites help you organize your devices by physical location.
|
|
</p>
|
|
<div class="mt-6">
|
|
<.button navigate={~p"/sites/new"} variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> Create Your First Site
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<%= if @grouped_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 class="bg-white dark:bg-gray-900">
|
|
<%= for {{site, devices, stats}, index} <- Enum.with_index(@grouped_devices) do %>
|
|
<tr
|
|
class={[
|
|
"border-t site-header",
|
|
if(index == 0, do: "border-gray-200", else: "border-gray-200"),
|
|
"dark:border-white/10"
|
|
]}
|
|
data-site-id={site.id}
|
|
data-site-position={index + 1}
|
|
draggable={if @reorder_mode, do: "true", else: "false"}
|
|
>
|
|
<td
|
|
:if={@reorder_mode}
|
|
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">
|
|
<.link
|
|
navigate={~p"/sites/#{site.id}"}
|
|
class="text-sm font-semibold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400"
|
|
>
|
|
{site.name}
|
|
</.link>
|
|
<span class="text-gray-400 dark:text-gray-600">·</span>
|
|
<span class="text-xs text-gray-600 dark:text-gray-400">
|
|
{stats.total} {if stats.total == 1, do: "device", else: "devices"}
|
|
</span>
|
|
<span
|
|
:if={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"
|
|
>
|
|
{stats.up} up
|
|
</span>
|
|
<span
|
|
:if={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"
|
|
>
|
|
{stats.down} down
|
|
</span>
|
|
</div>
|
|
<p
|
|
:if={site.location}
|
|
class="text-xs text-gray-500 dark:text-gray-400"
|
|
>
|
|
<.icon name="hero-map-pin" class="inline h-3 w-3" /> {site.location}
|
|
</p>
|
|
</div>
|
|
</th>
|
|
</tr>
|
|
<%= for {device, device_index} <- Enum.with_index(devices) do %>
|
|
<tr
|
|
class={[
|
|
"border-t device-row hover:bg-gray-50 dark:hover:bg-gray-800/50",
|
|
if(device_index == 0,
|
|
do: "border-gray-300 dark:border-white/15",
|
|
else: "border-gray-200 dark:border-white/10"
|
|
)
|
|
]}
|
|
data-device-id={device.id}
|
|
data-site-id={site.id}
|
|
data-device-position={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
|
|
phx-click={JS.navigate(~p"/devices/#{device.id}")}
|
|
class={[
|
|
"py-4 pr-3 text-sm font-medium whitespace-nowrap text-gray-900 dark:text-white cursor-pointer",
|
|
@reorder_mode && "pl-2",
|
|
!@reorder_mode && "pl-4 sm:pl-3"
|
|
]}
|
|
>
|
|
{device.name}
|
|
</td>
|
|
<td
|
|
phx-click={JS.navigate(~p"/devices/#{device.id}")}
|
|
class="px-3 py-4 text-sm whitespace-nowrap text-gray-500 dark:text-gray-400 font-mono cursor-pointer"
|
|
>
|
|
{device.ip_address}
|
|
</td>
|
|
<td
|
|
phx-click={JS.navigate(~p"/devices/#{device.id}")}
|
|
class="px-3 py-4 text-sm whitespace-nowrap cursor-pointer"
|
|
>
|
|
<span class={[
|
|
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
|
device.status == :up &&
|
|
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
device.status == :down &&
|
|
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
|
device.status == :unknown &&
|
|
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
|
]}>
|
|
{device.status |> to_string() |> String.upcase()}
|
|
</span>
|
|
</td>
|
|
<td
|
|
phx-click={JS.navigate(~p"/devices/#{device.id}")}
|
|
class="px-3 py-4 text-sm whitespace-nowrap text-gray-500 dark:text-gray-400 cursor-pointer"
|
|
>
|
|
<.timestamp datetime={device.last_checked_at} timezone={@timezone} />
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<% 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 %>
|
|
<.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>
|
|
<% end %>
|
|
<% end %>
|
|
</Layouts.authenticated>
|