device list update
This commit is contained in:
parent
2f24489ecb
commit
e781a70c7e
4 changed files with 182 additions and 279 deletions
|
|
@ -12,36 +12,19 @@ defmodule ToweropsWeb.DeviceLive.Index do
|
|||
organization = socket.assigns.current_organization
|
||||
device = Devices.list_organization_devices(organization.id)
|
||||
sites = Sites.list_organization_sites(organization.id)
|
||||
grouped_devices = group_devices_by_site(device)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Devices")
|
||||
|> assign(:device, device)
|
||||
|> assign(:grouped_devices, grouped_devices)
|
||||
|> assign(:has_sites, sites != [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
tab = Map.get(params, "tab", "existing")
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params, tab)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params, tab) do
|
||||
organization = socket.assigns.current_organization
|
||||
|
||||
case tab do
|
||||
"discovered" ->
|
||||
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
||||
|
||||
socket
|
||||
|> assign(:active_tab, "discovered")
|
||||
|> assign(:discovered_devices, discovered)
|
||||
|
||||
_ ->
|
||||
socket
|
||||
|> assign(:active_tab, "existing")
|
||||
|> assign(:discovered_devices, [])
|
||||
end
|
||||
def handle_params(_params, _url, socket) do
|
||||
{:noreply, assign(socket, :live_action, :index)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -83,15 +66,24 @@ defmodule ToweropsWeb.DeviceLive.Index do
|
|||
end
|
||||
end
|
||||
|
||||
# Device type icon helpers
|
||||
defp device_type_icon(:router), do: "hero-signal"
|
||||
defp device_type_icon(:switch), do: "hero-squares-2x2"
|
||||
defp device_type_icon(:wireless), do: "hero-wifi"
|
||||
defp device_type_icon(:server), do: "hero-server"
|
||||
defp device_type_icon(:workstation), do: "hero-computer-desktop"
|
||||
defp device_type_icon(_), do: "hero-question-mark-circle"
|
||||
# Group devices by site with statistics
|
||||
defp group_devices_by_site(devices) do
|
||||
devices
|
||||
|> Enum.group_by(& &1.site)
|
||||
|> Enum.map(fn {site, site_devices} ->
|
||||
{site, site_devices, calculate_site_stats(site_devices)}
|
||||
end)
|
||||
|> Enum.sort_by(fn {site, _devices, _stats} -> site.name end, :asc)
|
||||
end
|
||||
|
||||
defp device_type_label(type) do
|
||||
type |> to_string() |> String.capitalize()
|
||||
defp calculate_site_stats(devices) do
|
||||
status_counts = Enum.frequencies_by(devices, & &1.status)
|
||||
|
||||
%{
|
||||
total: length(devices),
|
||||
up: Map.get(status_counts, :up, 0),
|
||||
down: Map.get(status_counts, :down, 0),
|
||||
unknown: Map.get(status_counts, :unknown, 0)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,247 +27,158 @@
|
|||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<!-- 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>
|
||||
<%= 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">
|
||||
<thead class="bg-white dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-gray-900 sm:pl-3 dark:text-white"
|
||||
>
|
||||
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",
|
||||
if(index == 0, do: "border-gray-200", else: "border-gray-200"),
|
||||
"dark:border-white/10"
|
||||
]}>
|
||||
<th
|
||||
scope="colgroup"
|
||||
colspan="4"
|
||||
class="bg-gray-50 py-2 pr-3 pl-4 text-left sm:pl-3 dark:bg-gray-800/50"
|
||||
>
|
||||
<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
|
||||
phx-click={JS.navigate(~p"/devices/#{device.id}")}
|
||||
class={[
|
||||
"border-t cursor-pointer 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"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<td class="py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-3 dark:text-white">
|
||||
{device.name}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-500 dark:text-gray-400 font-mono">
|
||||
{device.ip_address}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm whitespace-nowrap">
|
||||
<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 class="px-3 py-4 text-sm whitespace-nowrap text-gray-500 dark:text-gray-400">
|
||||
<.timestamp datetime={device.last_checked_at} timezone={@timezone} />
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= if @device == [] 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 %>
|
||||
<.table
|
||||
id="devices"
|
||||
rows={@device}
|
||||
row_click={
|
||||
fn eq ->
|
||||
JS.navigate(~p"/devices/#{eq.id}")
|
||||
end
|
||||
}
|
||||
>
|
||||
<:col :let={eq} label="Name">
|
||||
<span class="font-medium">{eq.name}</span>
|
||||
</:col>
|
||||
<:col :let={eq} label="IP Address">
|
||||
<span class="font-mono text-sm">{eq.ip_address}</span>
|
||||
</:col>
|
||||
<:col :let={eq} label="Site">
|
||||
<.link
|
||||
navigate={~p"/sites/#{eq.site.id}"}
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{eq.site.name}
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={eq} label="Status">
|
||||
<span class={[
|
||||
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
||||
eq.status == :up &&
|
||||
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
||||
eq.status == :down && "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
||||
eq.status == :unknown &&
|
||||
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
||||
]}>
|
||||
{eq.status |> to_string() |> String.upcase()}
|
||||
</span>
|
||||
</:col>
|
||||
<:col :let={eq} label="Last Checked">
|
||||
<.timestamp
|
||||
datetime={eq.last_checked_at}
|
||||
timezone={@timezone}
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</:col>
|
||||
</.table>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% "discovered" -> %>
|
||||
<%= if @discovered_devices == [] do %>
|
||||
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12">
|
||||
<div class="text-center">
|
||||
<.icon name="hero-magnifying-glass" class="mx-auto h-12 w-12 text-gray-400" />
|
||||
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
No undiscovered devices
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
All discovered devices have been added, or no devices have been discovered yet via LLDP, CDP, or ARP.
|
||||
</p>
|
||||
</div>
|
||||
</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}
|
||||
timezone={@timezone}
|
||||
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 %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</Layouts.authenticated>
|
||||
|
|
|
|||
|
|
@ -209,14 +209,6 @@ defmodule ToweropsWeb.Router do
|
|||
|
||||
live "/", DashboardLive, :index
|
||||
live "/settings", Org.SettingsLive, :index
|
||||
|
||||
# Alert routes
|
||||
live "/alerts", AlertLive.Index, :index
|
||||
|
||||
# Agent routes
|
||||
live "/agents", AgentLive.Index, :index
|
||||
live "/agents/:id/edit", AgentLive.Edit, :edit
|
||||
live "/agents/:id", AgentLive.Show, :show
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -228,6 +220,14 @@ defmodule ToweropsWeb.Router do
|
|||
scope "/", ToweropsWeb do
|
||||
pipe_through [:browser, :require_authenticated_user]
|
||||
|
||||
# Alert routes
|
||||
live "/alerts", AlertLive.Index, :index
|
||||
|
||||
# Agent routes
|
||||
live "/agents", AgentLive.Index, :index
|
||||
live "/agents/:id/edit", AgentLive.Edit, :edit
|
||||
live "/agents/:id", AgentLive.Show, :show
|
||||
|
||||
# Site routes
|
||||
live "/sites", SiteLive.Index, :index
|
||||
live "/sites/new", SiteLive.Form, :new
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ defmodule ToweropsWeb.UserAuth do
|
|||
# Get user's organizations (ordered by most recently joined first)
|
||||
case Towerops.Organizations.list_user_organizations(user.id) do
|
||||
[_first_org | _] ->
|
||||
# Devices page (no longer needs org slug)
|
||||
# Devices page
|
||||
~p"/devices"
|
||||
|
||||
[] ->
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue