From e781a70c7e8440131212e3ae5382135c9721e357 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 26 Jan 2026 09:44:08 -0600 Subject: [PATCH] device list update --- lib/towerops_web/live/device_live/index.ex | 52 +-- .../live/device_live/index.html.heex | 391 +++++++----------- lib/towerops_web/router.ex | 16 +- lib/towerops_web/user_auth.ex | 2 +- 4 files changed, 182 insertions(+), 279 deletions(-) diff --git a/lib/towerops_web/live/device_live/index.ex b/lib/towerops_web/live/device_live/index.ex index 16431385..cd8c0db2 100644 --- a/lib/towerops_web/live/device_live/index.ex +++ b/lib/towerops_web/live/device_live/index.ex @@ -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 diff --git a/lib/towerops_web/live/device_live/index.html.heex b/lib/towerops_web/live/device_live/index.html.heex index 793aa122..d6d37cb9 100644 --- a/lib/towerops_web/live/device_live/index.html.heex +++ b/lib/towerops_web/live/device_live/index.html.heex @@ -27,247 +27,158 @@ - - -
- -
- - <%= case @active_tab do %> - <% "existing" -> %> - <%= if !@has_sites do %> -
- <.icon - name="hero-building-office" - class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" - /> -

- Create a site first -

-

- Before adding device, you need to create at least one site location. -

-

- Sites help you organize your devices by physical location. -

-
- <.button navigate={~p"/sites/new"} variant="primary"> - <.icon name="hero-plus" class="h-5 w-5" /> Create Your First Site - + <%= if !@has_sites do %> +
+ <.icon + name="hero-building-office" + class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" + /> +

+ Create a site first +

+

+ Before adding device, you need to create at least one site location. +

+

+ Sites help you organize your devices by physical location. +

+
+ <.button navigate={~p"/sites/new"} variant="primary"> + <.icon name="hero-plus" class="h-5 w-5" /> Create Your First Site + +
+
+ <% else %> + <%= if @grouped_devices == [] do %> +
+ <.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" /> +

No devices

+

+ Get started by adding your first device. +

+
+ <.button navigate={~p"/devices/new"} variant="primary"> + <.icon name="hero-plus" class="h-5 w-5" /> New Device + +
+
+ <% else %> +
+
+
+ + + + + + + + + + + <%= for {{site, devices, stats}, index} <- Enum.with_index(@grouped_devices) do %> + + + + <%= for {device, device_index} <- Enum.with_index(devices) do %> + + + + + + + <% end %> + <% end %> + +
+ Name + + IP Address + + Status + + Last Checked +
+
+
+ <.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} + + ยท + + {stats.total} {if stats.total == 1, do: "device", else: "devices"} + + 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 + + 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 + +
+

+ <.icon name="hero-map-pin" class="inline h-3 w-3" /> {site.location} +

+
+
+ {device.name} + + {device.ip_address} + + + {device.status |> to_string() |> String.upcase()} + + + <.timestamp datetime={device.last_checked_at} timezone={@timezone} /> +
- <% else %> - <%= if @device == [] do %> -
- <.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" /> -

No devices

-

- Get started by adding your first device. -

-
- <.button - navigate={~p"/devices/new"} - variant="primary" - > - <.icon name="hero-plus" class="h-5 w-5" /> New Device - -
-
- <% else %> - <.table - id="devices" - rows={@device} - row_click={ - fn eq -> - JS.navigate(~p"/devices/#{eq.id}") - end - } - > - <:col :let={eq} label="Name"> - {eq.name} - - <:col :let={eq} label="IP Address"> - {eq.ip_address} - - <: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} - - - <:col :let={eq} label="Status"> - - {eq.status |> to_string() |> String.upcase()} - - - <:col :let={eq} label="Last Checked"> - <.timestamp - datetime={eq.last_checked_at} - timezone={@timezone} - class="text-sm text-gray-600 dark:text-gray-400" - /> - - - <% end %> - <% end %> - <% "discovered" -> %> - <%= if @discovered_devices == [] do %> -
-
- <.icon name="hero-magnifying-glass" class="mx-auto h-12 w-12 text-gray-400" /> -

- No undiscovered devices -

-

- All discovered devices have been added, or no devices have been discovered yet via LLDP, CDP, or ARP. -

-
-
- <% else %> - <.table id="discovered-devices" rows={@discovered_devices}> - <:col :let={discovered} label="Identifier"> - <%= case discovered.identifier.type do %> - <% :mac -> %> -
- - MAC - - - {discovered.identifier.value} - -
- <% :ip -> %> -
- - IP - - - {discovered.identifier.value} - -
- <% :hostname -> %> -
- - HOST - - - {discovered.identifier.value} - -
- <% _ -> %> - Unknown - <% end %> - - - <:col :let={discovered} label="Hostname"> - <%= if discovered.hostname do %> - - {discovered.hostname} - - <% else %> - - - <% end %> - - - <:col :let={discovered} label="Type"> -
- <.icon - name={device_type_icon(discovered.device_type)} - class="h-4 w-4 text-gray-500" - /> - - {device_type_label(discovered.device_type)} - -
- - - <:col :let={discovered} label="Manufacturer"> - <%= if discovered.manufacturer do %> - - {discovered.manufacturer} - - <% else %> - - - <% end %> - - - <:col :let={discovered} label="Discovered By"> -
- <%= 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} - - ({first.interface}) - <% else %> - {length(discovered.discovered_by)} devices - <% end %> -
- - - <:col :let={discovered} label="Last Seen"> - <.timestamp - datetime={discovered.last_seen} - timezone={@timezone} - class="text-sm text-gray-600 dark:text-gray-400" - /> - - - <: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 - - - - <% end %> +
+ <% end %> <% end %> diff --git a/lib/towerops_web/router.ex b/lib/towerops_web/router.ex index 42406066..9517b104 100644 --- a/lib/towerops_web/router.ex +++ b/lib/towerops_web/router.ex @@ -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 diff --git a/lib/towerops_web/user_auth.ex b/lib/towerops_web/user_auth.ex index 437b5bbb..dc91025a 100644 --- a/lib/towerops_web/user_auth.ex +++ b/lib/towerops_web/user_auth.ex @@ -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" [] ->