Replace the revoke functionality with delete to properly clean up agent references. When an agent is deleted: - All direct device assignments are removed - Agent is removed from site defaults - Agent is removed from organization defaults - Devices automatically fall back to site/org agents or cloud polling This ensures no orphaned references and provides better clarity about what happens to device monitoring when an agent is removed. - Add delete_agent_token/1 function in Agents context - Update LiveView to use delete_agent event instead of revoke_agent - Update UI button from "Revoke" to "Delete" with clearer confirmation message - Add comprehensive tests for delete functionality and fallback behavior - All 792 tests passing, no Credo warnings
177 lines
6.2 KiB
Elixir
177 lines
6.2 KiB
Elixir
defmodule ToweropsWeb.AgentLive.Index do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
import ToweropsWeb.AgentLive.Helpers
|
|
|
|
alias Towerops.Agents
|
|
alias Towerops.Agents.Stats
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
organization = socket.assigns.current_organization
|
|
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
|
|
|
# device counts for each agent (both direct and total with inheritance)
|
|
equipment_counts =
|
|
Map.new(agent_tokens, fn token ->
|
|
direct = Agents.count_assigned_devices(token.id)
|
|
total = length(Agents.list_agent_polling_targets(token.id))
|
|
{token.id, %{direct: direct, total: total}}
|
|
end)
|
|
|
|
# Get organization-wide agent health statistics
|
|
agent_health_stats = Stats.get_organization_agent_health(organization.id)
|
|
assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id)
|
|
offline_agents = Stats.get_offline_agents(organization.id)
|
|
|
|
# Get agent image URL from config or use default
|
|
agent_image = Application.get_env(:towerops, :agent_docker_image, "towerops/agent:latest")
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, "Remote Agents")
|
|
|> assign(:agent_tokens, agent_tokens)
|
|
|> assign(:device_counts, equipment_counts)
|
|
|> assign(:agent_health_stats, agent_health_stats)
|
|
|> assign(:assignment_breakdown, assignment_breakdown)
|
|
|> assign(:offline_agents, offline_agents)
|
|
|> assign(:agent_image, agent_image)
|
|
|> assign(:new_token, nil)
|
|
|> assign(:show_token_modal, false)
|
|
|> assign(:agent_form, to_form(%{"name" => ""}))}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("create_agent", params, socket) do
|
|
name =
|
|
case params do
|
|
%{"agent_form" => %{"name" => n}} -> n
|
|
%{"name" => n} -> n
|
|
end
|
|
|
|
organization = socket.assigns.current_organization
|
|
|
|
case Agents.create_agent_token(organization.id, name) do
|
|
{:ok, agent_token, token} ->
|
|
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
|
|
|
# device counts
|
|
equipment_counts =
|
|
Map.new(agent_tokens, fn t ->
|
|
direct = Agents.count_assigned_devices(t.id)
|
|
total = length(Agents.list_agent_polling_targets(t.id))
|
|
{t.id, %{direct: direct, total: total}}
|
|
end)
|
|
|
|
# Refresh health statistics
|
|
agent_health_stats = Stats.get_organization_agent_health(organization.id)
|
|
assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id)
|
|
offline_agents = Stats.get_offline_agents(organization.id)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:agent_tokens, agent_tokens)
|
|
|> assign(:device_counts, equipment_counts)
|
|
|> assign(:agent_health_stats, agent_health_stats)
|
|
|> assign(:assignment_breakdown, assignment_breakdown)
|
|
|> assign(:offline_agents, offline_agents)
|
|
|> assign(:new_token, %{agent_token: agent_token, token: token})
|
|
|> assign(:show_token_modal, true)
|
|
|> assign(:agent_form, to_form(%{"name" => ""}))
|
|
|> put_flash(:info, "Agent created successfully")}
|
|
|
|
{:error, _changeset} ->
|
|
{:noreply, put_flash(socket, :error, "Failed to create agent")}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("close_token_modal", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> assign(:show_token_modal, false)
|
|
|> assign(:new_token, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("show_setup", %{"id" => id}, socket) do
|
|
agent_token = Agents.get_agent_token!(id)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:new_token, %{agent_token: agent_token, token: agent_token.token})
|
|
|> assign(:show_token_modal, true)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("regenerate_token", %{"id" => id}, socket) do
|
|
agent_token = Agents.get_agent_token!(id)
|
|
organization = socket.assigns.current_organization
|
|
|
|
# Revoke the old token and create a new one
|
|
case Agents.revoke_agent_token(id) do
|
|
{:ok, _} ->
|
|
case Agents.create_agent_token(organization.id, agent_token.name) do
|
|
{:ok, new_agent_token, token} ->
|
|
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:agent_tokens, agent_tokens)
|
|
|> assign(:new_token, %{agent_token: new_agent_token, token: token})
|
|
|> assign(:show_token_modal, true)
|
|
|> put_flash(:info, "New token generated successfully")}
|
|
|
|
{:error, _changeset} ->
|
|
{:noreply, put_flash(socket, :error, "Failed to generate new token")}
|
|
end
|
|
|
|
{:error, _} ->
|
|
{:noreply, put_flash(socket, :error, "Failed to revoke old token")}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("delete_agent", %{"id" => id}, socket) do
|
|
case Agents.delete_agent_token(id) do
|
|
{:ok, _} ->
|
|
organization = socket.assigns.current_organization
|
|
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
|
|
|
# Recalculate device counts after deletion
|
|
equipment_counts =
|
|
Map.new(agent_tokens, fn token ->
|
|
direct = Agents.count_assigned_devices(token.id)
|
|
total = length(Agents.list_agent_polling_targets(token.id))
|
|
{token.id, %{direct: direct, total: total}}
|
|
end)
|
|
|
|
# Refresh health statistics
|
|
agent_health_stats = Stats.get_organization_agent_health(organization.id)
|
|
assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id)
|
|
offline_agents = Stats.get_offline_agents(organization.id)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:agent_tokens, agent_tokens)
|
|
|> assign(:device_counts, equipment_counts)
|
|
|> assign(:agent_health_stats, agent_health_stats)
|
|
|> assign(:assignment_breakdown, assignment_breakdown)
|
|
|> assign(:offline_agents, offline_agents)
|
|
|> put_flash(:info, "Agent deleted successfully. Devices now fall back to site/org defaults or cloud polling.")}
|
|
|
|
{:error, _} ->
|
|
{:noreply, put_flash(socket, :error, "Failed to delete agent")}
|
|
end
|
|
end
|
|
|
|
defp apply_action(socket, :index, _params) do
|
|
socket
|
|
end
|
|
end
|