Group cloud pollers and org agents separately on admin agents page

Cloud pollers render in their own table at the top, org agents
below with the organization name shown under each agent name.
This commit is contained in:
Graham McIntire 2026-02-10 11:07:41 -06:00
parent 6a66203e48
commit c6caa81850
No known key found for this signature in database
2 changed files with 152 additions and 70 deletions

View file

@ -14,13 +14,17 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do
end
agent_tokens = Agents.list_all_agent_tokens()
{cloud_pollers, org_agents} = Enum.split_with(agent_tokens, & &1.is_cloud_poller)
device_counts = calculate_device_counts(agent_tokens)
{:ok,
socket
|> assign(:page_title, "All Agents")
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> stream(:agent_tokens, agent_tokens)
|> stream(:cloud_pollers, cloud_pollers)
|> stream(:org_agents, org_agents)
|> assign(:has_cloud_pollers, cloud_pollers != [])
|> assign(:has_org_agents, org_agents != [])
|> assign(:device_counts, device_counts)
|> assign(:now, DateTime.utc_now())}
end
@ -56,8 +60,10 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do
direct = Agents.count_assigned_devices(agent_token_id)
total = length(Agents.list_agent_polling_targets(agent_token_id))
stream_name = if agent_token.is_cloud_poller, do: :cloud_pollers, else: :org_agents
socket
|> stream_insert(:agent_tokens, agent_token)
|> stream_insert(stream_name, agent_token)
|> assign(:device_counts, Map.put(socket.assigns.device_counts, agent_token_id, %{direct: direct, total: total}))
rescue
Ecto.NoResultsError -> socket
@ -65,10 +71,14 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do
defp reload_all(socket) do
agent_tokens = Agents.list_all_agent_tokens()
{cloud_pollers, org_agents} = Enum.split_with(agent_tokens, & &1.is_cloud_poller)
device_counts = calculate_device_counts(agent_tokens)
socket
|> stream(:agent_tokens, agent_tokens, reset: true)
|> stream(:cloud_pollers, cloud_pollers, reset: true)
|> stream(:org_agents, org_agents, reset: true)
|> assign(:has_cloud_pollers, cloud_pollers != [])
|> assign(:has_org_agents, org_agents != [])
|> assign(:device_counts, device_counts)
end

View file

@ -7,77 +7,149 @@
</p>
</div>
<.table id="admin-agents-table" rows={@streams.agent_tokens}>
<:col :let={{_id, agent}} label="Name">
<div class="flex items-center gap-2">
<.link
navigate={~p"/agents/#{agent.id}"}
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{agent.name}
</.link>
<%= if agent.is_cloud_poller do %>
<span class="inline-flex items-center gap-1 rounded-full bg-purple-50 dark:bg-purple-900/30 px-2 py-0.5 text-xs font-medium text-purple-700 dark:text-purple-300 ring-1 ring-inset ring-purple-600/20 dark:ring-purple-400/30">
<.icon name="hero-cloud" class="h-3 w-3" /> Cloud
<%!-- Cloud Pollers --%>
<%= if @has_cloud_pollers do %>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
<.icon name="hero-cloud" class="w-5 h-5 inline mr-1" /> Cloud Pollers
</h2>
<.table id="cloud-pollers-table" rows={@streams.cloud_pollers}>
<:col :let={{_id, agent}} label="Name">
<div class="flex items-center gap-2">
<.link
navigate={~p"/agents/#{agent.id}"}
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{agent.name}
</.link>
<%= if agent.allow_remote_debug do %>
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300 ring-1 ring-inset ring-amber-600/20 dark:ring-amber-400/30">
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
</span>
<% end %>
</div>
</:col>
<:col :let={{_id, agent}} label="Status">
<% {status, label} = agent_status(agent) %>
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
{label}
</span>
<% end %>
<%= if agent.allow_remote_debug do %>
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300 ring-1 ring-inset ring-amber-600/20 dark:ring-amber-400/30">
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
</:col>
<:col :let={{_id, agent}} label="Devices">
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{counts.direct} direct
<%= if counts.total > counts.direct do %>
· {counts.total - counts.direct} inherited
<% end %>
</div>
</:col>
<:col :let={{_id, agent}} label="Last Seen">
<div class="text-sm text-gray-900 dark:text-white">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
</div>
<%= if agent.last_seen_at do %>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
<.timestamp
datetime={agent.last_seen_at}
timezone={@timezone}
format="absolute"
/>
</div>
<% end %>
</:col>
<:col :let={{_id, agent}} label="Version">
<%= if agent.metadata["version"] do %>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
</div>
<% end %>
</:col>
</.table>
</div>
<% end %>
<%!-- Organization Agents --%>
<%= if @has_org_agents do %>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
<.icon name="hero-server" class="w-5 h-5 inline mr-1" /> Organization Agents
</h2>
<.table id="org-agents-table" rows={@streams.org_agents}>
<:col :let={{_id, agent}} label="Name">
<div>
<div class="flex items-center gap-2">
<.link
navigate={~p"/agents/#{agent.id}"}
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{agent.name}
</.link>
<%= if agent.allow_remote_debug do %>
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300 ring-1 ring-inset ring-amber-600/20 dark:ring-amber-400/30">
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
</span>
<% end %>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{agent.organization && agent.organization.name}
</div>
</div>
</:col>
<:col :let={{_id, agent}} label="Status">
<% {status, label} = agent_status(agent) %>
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
{label}
</span>
<% end %>
</div>
</:col>
</:col>
<:col :let={{_id, agent}} label="Organization">
<%= if agent.is_cloud_poller do %>
<span class="text-sm text-purple-600 dark:text-purple-400">Cloud Poller</span>
<% else %>
<span class="text-sm text-gray-900 dark:text-white">
{agent.organization && agent.organization.name}
</span>
<% end %>
</:col>
<:col :let={{_id, agent}} label="Devices">
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{counts.direct} direct
<%= if counts.total > counts.direct do %>
· {counts.total - counts.direct} inherited
<% end %>
</div>
</:col>
<:col :let={{_id, agent}} label="Status">
<% {status, label} = agent_status(agent) %>
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
{label}
</span>
</:col>
<:col :let={{_id, agent}} label="Last Seen">
<div class="text-sm text-gray-900 dark:text-white">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
</div>
<%= if agent.last_seen_at do %>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
<.timestamp
datetime={agent.last_seen_at}
timezone={@timezone}
format="absolute"
/>
</div>
<% end %>
</:col>
<:col :let={{_id, agent}} label="Devices">
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{counts.direct} direct
<%= if counts.total > counts.direct do %>
· {counts.total - counts.direct} inherited
<% end %>
</div>
</:col>
<:col :let={{_id, agent}} label="Last Seen">
<div class="text-sm text-gray-900 dark:text-white">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
</div>
<%= if agent.last_seen_at do %>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} format="absolute" />
</div>
<% end %>
</:col>
<:col :let={{_id, agent}} label="Version">
<%= if agent.metadata["version"] do %>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
</div>
<% end %>
</:col>
</.table>
<:col :let={{_id, agent}} label="Version">
<%= if agent.metadata["version"] do %>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
</div>
<% end %>
</:col>
</.table>
</div>
<% end %>
</div>
</Layouts.admin>