Device list bug fix
This commit is contained in:
parent
4f936d5549
commit
89911bae9c
4 changed files with 227 additions and 215 deletions
|
|
@ -88,37 +88,11 @@ defmodule ToweropsWeb.Api.V1.DevicesController do
|
|||
organization_id = conn.assigns.current_organization_id
|
||||
current_user = conn.assigns[:current_user]
|
||||
|
||||
# Default organization_id to authenticated org if not provided or empty
|
||||
device_params =
|
||||
case Map.get(device_params, "organization_id") do
|
||||
nil -> Map.put(device_params, "organization_id", organization_id)
|
||||
"" -> Map.put(device_params, "organization_id", organization_id)
|
||||
_provided_id -> device_params
|
||||
end
|
||||
device_params = default_organization_id(device_params, organization_id)
|
||||
|
||||
# Verify site belongs to organization if site_id is provided
|
||||
case verify_site_access(device_params["site_id"], organization_id) do
|
||||
:ok ->
|
||||
# Superusers bypass device quota limits
|
||||
opts = if current_user && current_user.is_superuser, do: [bypass_limits: true], else: []
|
||||
|
||||
case Devices.create_device(device_params, opts) do
|
||||
{:ok, device} ->
|
||||
# Trigger SNMP discovery if enabled
|
||||
_ =
|
||||
if device.snmp_enabled do
|
||||
DiscoveryWorker.enqueue(device.id)
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(format_device(device))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
create_and_discover_device(conn, device_params, current_user)
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|
|
@ -265,6 +239,38 @@ defmodule ToweropsWeb.Api.V1.DevicesController do
|
|||
|
||||
# Private helpers
|
||||
|
||||
defp default_organization_id(device_params, organization_id) do
|
||||
case Map.get(device_params, "organization_id") do
|
||||
nil -> Map.put(device_params, "organization_id", organization_id)
|
||||
"" -> Map.put(device_params, "organization_id", organization_id)
|
||||
_provided_id -> device_params
|
||||
end
|
||||
end
|
||||
|
||||
defp create_and_discover_device(conn, device_params, current_user) do
|
||||
opts = if current_user && current_user.is_superuser, do: [bypass_limits: true], else: []
|
||||
|
||||
case Devices.create_device(device_params, opts) do
|
||||
{:ok, device} ->
|
||||
maybe_enqueue_discovery(device)
|
||||
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(format_device(device))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_enqueue_discovery(device) do
|
||||
if device.snmp_enabled do
|
||||
DiscoveryWorker.enqueue(device.id)
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_site_access(nil, _organization_id), do: :ok
|
||||
|
||||
defp verify_site_access(site_id, organization_id) do
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ defmodule ToweropsWeb.DeviceLive.Index do
|
|||
|> assign(:timezone, socket.assigns.current_scope.timezone)
|
||||
|> assign(:device, device)
|
||||
|> assign(:grouped_devices, grouped_devices)
|
||||
|> assign(:sites_enabled, organization.use_sites)
|
||||
|> assign(:has_sites, sites != [])
|
||||
|> assign(:reorder_mode, false)
|
||||
|> assign(:device_quota, %{current: current, limit: limit})}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
<% end %>
|
||||
|
||||
<.button
|
||||
:if={@has_sites}
|
||||
:if={!@sites_enabled || @has_sites}
|
||||
navigate={~p"/devices/new"}
|
||||
variant="primary"
|
||||
>
|
||||
|
|
@ -137,227 +137,229 @@
|
|||
|
||||
<%= case @active_tab do %>
|
||||
<% "existing" -> %>
|
||||
<%= if !@has_sites do %>
|
||||
<%= if @sites_enabled && !@has_sites && @grouped_devices != [] do %>
|
||||
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon name="hero-information-circle" class="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Organize your devices with sites
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
||||
<p>
|
||||
Sites help you organize devices by physical location. Create a site to assign your devices and keep things organized.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<.button navigate={~p"/sites/new"}>
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> Create Your First Site
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @grouped_devices == [] 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>
|
||||
<.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">
|
||||
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.
|
||||
Get started by adding your first device.
|
||||
</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 navigate={~p"/devices/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="h-5 w-5" /> New Device
|
||||
</.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"
|
||||
<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={if site, do: site.id, else: "no-site"}
|
||||
data-site-position={index + 1}
|
||||
draggable={if @reorder_mode && site, do: "true", else: "false"}
|
||||
>
|
||||
<td
|
||||
:if={@reorder_mode && site}
|
||||
class="py-2 pl-4 pr-2 sm:pl-3 bg-gray-50 dark:bg-gray-800/50"
|
||||
>
|
||||
<span class="sr-only">Drag handle</span>
|
||||
</th>
|
||||
<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="col"
|
||||
scope="colgroup"
|
||||
colspan="4"
|
||||
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"
|
||||
"bg-gray-50 py-2 pr-3 text-left dark:bg-gray-800/50",
|
||||
!@reorder_mode && "pl-4 sm:pl-3"
|
||||
]}
|
||||
>
|
||||
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
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= if site do %>
|
||||
<.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>
|
||||
<% else %>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{if @sites_enabled, do: "Unassigned Devices", else: "All Devices"}
|
||||
</span>
|
||||
<% end %>
|
||||
<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 && 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>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900">
|
||||
<%= for {{site, devices, stats}, index} <- Enum.with_index(@grouped_devices) do %>
|
||||
<%= for {device, device_index} <- Enum.with_index(devices) do %>
|
||||
<tr
|
||||
class={[
|
||||
"border-t site-header",
|
||||
if(index == 0, do: "border-gray-200", else: "border-gray-200"),
|
||||
"dark:border-white/10"
|
||||
"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={if site, do: site.id, else: "no-site"}
|
||||
data-site-position={index + 1}
|
||||
draggable={if @reorder_mode && site, do: "true", else: "false"}
|
||||
data-device-position={device_index + 1}
|
||||
draggable={if @reorder_mode, do: "true", else: "false"}
|
||||
>
|
||||
<td
|
||||
:if={@reorder_mode && site}
|
||||
class="py-2 pl-4 pr-2 sm:pl-3 bg-gray-50 dark:bg-gray-800/50"
|
||||
>
|
||||
<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 site"
|
||||
title="Drag to reorder device"
|
||||
>
|
||||
<.icon name="hero-bars-3" class="h-5 w-5" />
|
||||
</button>
|
||||
</td>
|
||||
<th
|
||||
scope="colgroup"
|
||||
colspan="4"
|
||||
<td
|
||||
phx-click={JS.navigate(~p"/devices/#{device.id}")}
|
||||
class={[
|
||||
"bg-gray-50 py-2 pr-3 text-left dark:bg-gray-800/50",
|
||||
"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"
|
||||
]}
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= if site do %>
|
||||
<.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>
|
||||
<% else %>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
All Devices
|
||||
</span>
|
||||
<% end %>
|
||||
<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 && 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={if site, do: site.id, else: "no-site"}
|
||||
data-device-position={device_index + 1}
|
||||
draggable={if @reorder_mode, do: "true", else: "false"}
|
||||
{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"
|
||||
>
|
||||
<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 %>
|
||||
{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 %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% "discovered" -> %>
|
||||
<%= if @discovered_devices == [] do %>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ Devices Tested & Working
|
|||
2026-02-04
|
||||
* Feature: SNMP v3 support
|
||||
* Sites are now optional and not enabled by default
|
||||
* Ability to set an org as default
|
||||
* Poller: snmp v3 support
|
||||
* Poller: ensure credentials do not remain in memory
|
||||
|
||||
2026-02-03
|
||||
* More tests
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue