266 lines
12 KiB
Text
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>
|