Fix agent page jumping by using streams for real-time updates
- Convert agent_tokens from assign to stream to prevent full page re-renders - Update handle_info callbacks to only update the specific agent that changed - Only update stats when agent status changes, not device counts - Prevents IP address from temporarily disappearing during updates
This commit is contained in:
parent
39fa1c6e4e
commit
ae9b393137
2 changed files with 47 additions and 24 deletions
|
|
@ -55,7 +55,7 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
|> assign(:page_title, "Remote Agents")
|
||||
|> assign(:timezone, socket.assigns.current_scope.timezone)
|
||||
|> assign(:is_superuser, is_superuser)
|
||||
|> assign(:agent_tokens, agent_tokens)
|
||||
|> stream(:agent_tokens, agent_tokens)
|
||||
|> assign(:cloud_pollers, cloud_pollers)
|
||||
|> assign(:global_default_cloud_poller_id, global_default_cloud_poller_id)
|
||||
|> assign(:selected_global_default, global_default_cloud_poller_id || "")
|
||||
|
|
@ -355,18 +355,18 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
|
||||
# Handle real-time agent status updates
|
||||
@impl true
|
||||
def handle_info({:agent_connected, _agent_token_id, _organization_id}, socket) do
|
||||
{:noreply, reload_agent_data(socket)}
|
||||
def handle_info({:agent_connected, agent_token_id, _organization_id}, socket) do
|
||||
{:noreply, update_single_agent(socket, agent_token_id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:agent_disconnected, _agent_token_id, _organization_id}, socket) do
|
||||
{:noreply, reload_agent_data(socket)}
|
||||
def handle_info({:agent_disconnected, agent_token_id, _organization_id}, socket) do
|
||||
{:noreply, update_single_agent(socket, agent_token_id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:agent_heartbeat, _agent_token_id, _organization_id}, socket) do
|
||||
{:noreply, reload_agent_data(socket)}
|
||||
def handle_info({:agent_heartbeat, agent_token_id, _organization_id}, socket) do
|
||||
{:noreply, update_single_agent(socket, agent_token_id)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -388,7 +388,7 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
offline_agents = Stats.get_offline_agents(organization.id)
|
||||
|
||||
socket
|
||||
|> assign(:agent_tokens, agent_tokens)
|
||||
|> stream(:agent_tokens, agent_tokens, reset: true)
|
||||
|> assign(:cloud_pollers, cloud_pollers)
|
||||
|> assign(:device_counts, equipment_counts)
|
||||
|> assign(:cloud_poller_counts, cloud_poller_counts)
|
||||
|
|
@ -396,4 +396,27 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
|> assign(:assignment_breakdown, assignment_breakdown)
|
||||
|> assign(:offline_agents, offline_agents)
|
||||
end
|
||||
|
||||
defp update_single_agent(socket, agent_token_id) do
|
||||
agent_token = Agents.get_agent_token!(agent_token_id)
|
||||
|
||||
socket
|
||||
|> stream_insert(:agent_tokens, agent_token)
|
||||
|> maybe_update_stats()
|
||||
rescue
|
||||
Ecto.NoResultsError ->
|
||||
# Agent was deleted, just update stats
|
||||
maybe_update_stats(socket)
|
||||
end
|
||||
|
||||
defp maybe_update_stats(socket) do
|
||||
organization = socket.assigns.current_scope.organization
|
||||
|
||||
agent_health_stats = Stats.get_organization_agent_health(organization.id)
|
||||
offline_agents = Stats.get_offline_agents(organization.id)
|
||||
|
||||
socket
|
||||
|> assign(:agent_health_stats, agent_health_stats)
|
||||
|> assign(:offline_agents, offline_agents)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @agent_tokens == [] do %>
|
||||
<%= if Enum.empty?(@streams.agent_tokens) 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 agents</h3>
|
||||
|
|
@ -67,8 +67,8 @@
|
|||
</div>
|
||||
<% else %>
|
||||
<div class="mt-6">
|
||||
<.table id="agents-table" rows={@agent_tokens}>
|
||||
<:col :let={agent} label="Name">
|
||||
<.table id="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}"}
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
</div>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Status">
|
||||
<: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)}"} />
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
</span>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Device">
|
||||
<:col :let={{_id, agent}} label="Device">
|
||||
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
|
||||
<div class="text-sm text-gray-900 dark:text-white">
|
||||
{counts.total} total
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
</div>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Last Seen">
|
||||
<: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} />
|
||||
</div>
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
<% end %>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Metadata">
|
||||
<:col :let={{_id, agent}} label="Metadata">
|
||||
<%= if agent.metadata["hostname"] do %>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<.icon name="hero-computer-desktop" class="h-4 w-4 inline" />
|
||||
|
|
@ -140,13 +140,13 @@
|
|||
<% end %>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Created">
|
||||
<:col :let={{_id, agent}} label="Created">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
|
||||
</div>
|
||||
</:col>
|
||||
|
||||
<:action :let={agent}>
|
||||
<:action :let={{_id, agent}}>
|
||||
<%= if agent.enabled do %>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
|
|
@ -185,7 +185,7 @@
|
|||
Application-wide agents that can poll devices for any organization. Only visible and manageable by superadmins.
|
||||
</p>
|
||||
<.table id="cloud-pollers-table" rows={@cloud_pollers}>
|
||||
<:col :let={agent} label="Name">
|
||||
<:col :let={{_id, agent}} label="Name">
|
||||
<div class="flex items-center gap-2">
|
||||
<.link
|
||||
navigate={~p"/agents/#{agent.id}"}
|
||||
|
|
@ -206,7 +206,7 @@
|
|||
</div>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Status">
|
||||
<: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)}"} />
|
||||
|
|
@ -214,7 +214,7 @@
|
|||
</span>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Device">
|
||||
<:col :let={{_id, agent}} label="Device">
|
||||
<% counts = Map.get(@cloud_poller_counts, agent.id, %{direct: 0, total: 0}) %>
|
||||
<div class="text-sm text-gray-900 dark:text-white">
|
||||
{counts.total} total
|
||||
|
|
@ -227,7 +227,7 @@
|
|||
</div>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Last Seen">
|
||||
<: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} />
|
||||
</div>
|
||||
|
|
@ -243,7 +243,7 @@
|
|||
<% end %>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Metadata">
|
||||
<:col :let={{_id, agent}} label="Metadata">
|
||||
<%= if agent.metadata["hostname"] do %>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<.icon name="hero-computer-desktop" class="h-4 w-4 inline" />
|
||||
|
|
@ -257,13 +257,13 @@
|
|||
<% end %>
|
||||
</:col>
|
||||
|
||||
<:col :let={agent} label="Created">
|
||||
<:col :let={{_id, agent}} label="Created">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
|
||||
</div>
|
||||
</:col>
|
||||
|
||||
<:action :let={agent}>
|
||||
<:action :let={{_id, agent}}>
|
||||
<%= if agent.enabled do %>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue