towerops/lib/towerops_web/live/device_live/index.html.heex
2026-01-26 10:22:12 -06:00

266 lines
12 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="devices"
timezone={@timezone}
>
<.header>
{@page_title}
<:subtitle>Monitor and manage all your network devices</:subtitle>
<:actions>
<%= 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>
<.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 %>
<% end %>
<.button
:if={@has_sites}
navigate={~p"/devices/new"}
variant="primary"
>
<.icon name="hero-plus" class="h-5 w-5" /> New Device
</.button>
</:actions>
</.header>
<%= 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",
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 hover:bg-gray-50 dark:hover:bg-gray-800/50",
@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 hover:bg-gray-50 dark:hover:bg-gray-800/50"
>
{device.ip_address}
</td>
<td
phx-click={JS.navigate(~p"/devices/#{device.id}")}
class="px-3 py-4 text-sm whitespace-nowrap cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"
>
<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 hover:bg-gray-50 dark:hover:bg-gray-800/50"
>
<.timestamp datetime={device.last_checked_at} timezone={@timezone} />
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<% end %>
<% end %>
</Layouts.authenticated>