Enhance agent assignment UI with clear inheritance indicators

- Add override warnings (amber) when site/equipment explicitly override defaults
- Show inherited agent names in organization and site forms
- Display equipment assignment breakdown in organization settings
- Make equipment boxes fully clickable on site page (remove View button)
- Add hover effects to equipment boxes for better UX
- Consistent icon usage across all assignment levels
This commit is contained in:
Graham McIntire 2026-01-14 16:20:20 -06:00
parent 8442d9e1fc
commit 8864b8d26d
No known key found for this signature in database
6 changed files with 106 additions and 50 deletions

View file

@ -62,34 +62,28 @@
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
<%= if @live_action == :edit and Map.has_key?(assigns, :agent_source) do %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
<%= case @agent_source do %>
<% :equipment -> %>
<span class="inline-flex items-center gap-1">
<.icon name="hero-server" class="h-4 w-4" />
Explicitly assigned to this equipment
</span>
<% :site -> %>
<span class="inline-flex items-center gap-1">
<.icon name="hero-building-office" class="h-4 w-4" /> Inherited from site
<%= if @effective_agent_name do %>
(<strong>{@effective_agent_name}</strong>)
<% end %>
</span>
<% :organization -> %>
<span class="inline-flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Inherited from organization
<%= if @effective_agent_name do %>
(<strong>{@effective_agent_name}</strong>)
<% end %>
</span>
<% :none -> %>
<span class="inline-flex items-center gap-1">
<.icon name="hero-cloud" class="h-4 w-4" /> No agent assigned - cloud polling
</span>
<% end %>
</p>
<%= case @agent_source do %>
<% :equipment -> %>
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>Overriding</strong> site/organization defaults -
this equipment is explicitly assigned
</p>
<% :site -> %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400 flex items-center gap-1">
<.icon name="hero-building-office" class="h-4 w-4" /> Inherited from site:
<strong>{@effective_agent_name}</strong>
</p>
<% :organization -> %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Inherited from organization: <strong>{@effective_agent_name}</strong>
</p>
<% :none -> %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400 flex items-center gap-1">
<.icon name="hero-cloud" class="h-4 w-4" /> No agent assigned - cloud polling
</p>
<% end %>
<% else %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Assign this equipment to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults.

View file

@ -10,12 +10,16 @@ defmodule ToweropsWeb.Org.SettingsLive do
organization = socket.assigns.current_organization
available_agents = Agents.list_organization_agent_tokens(organization.id)
# Get assignment breakdown to show impact
assignment_breakdown = Agents.Stats.get_equipment_assignment_breakdown(organization.id)
changeset = Organizations.change_organization(organization)
{:ok,
socket
|> assign(:organization, organization)
|> assign(:available_agents, available_agents)
|> assign(:assignment_breakdown, assignment_breakdown)
|> assign(:form, to_form(changeset))}
end

View file

@ -38,7 +38,36 @@
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
<p class="mt-2 text-sm text-zinc-500 dark:text-zinc-400 italic">
<div class="mt-3 p-3 bg-zinc-50 dark:bg-zinc-800 rounded-lg text-sm">
<p class="font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Current Equipment Assignment:
</p>
<div class="space-y-1 text-zinc-600 dark:text-zinc-400">
<div class="flex items-center gap-2">
<.icon name="hero-server" class="h-4 w-4" />
<span><strong>{@assignment_breakdown.direct}</strong> explicitly assigned</span>
</div>
<div class="flex items-center gap-2">
<.icon name="hero-building-office" class="h-4 w-4" />
<span><strong>{@assignment_breakdown.site}</strong> inherited from site</span>
</div>
<div class="flex items-center gap-2">
<.icon name="hero-building-office-2" class="h-4 w-4" />
<span>
<strong>{@assignment_breakdown.organization}</strong>
inheriting organization default
</span>
</div>
<div class="flex items-center gap-2">
<.icon name="hero-cloud" class="h-4 w-4" />
<span>
<strong>{@assignment_breakdown.cloud}</strong> cloud polling (no agent)
</span>
</div>
</div>
</div>
<p class="mt-3 text-sm text-zinc-500 dark:text-zinc-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
Agent assignment hierarchy: Equipment > Site > Organization
</p>

View file

@ -25,10 +25,19 @@ defmodule ToweropsWeb.SiteLive.Form do
defp apply_action(socket, :new, _params) do
changeset = Sites.change_site(%Site{organization_id: socket.assigns.organization.id})
# Get inherited agent info from organization
org_agent =
if socket.assigns.organization.default_agent_token_id do
Enum.find(socket.assigns.available_agents, fn a ->
a.id == socket.assigns.organization.default_agent_token_id
end)
end
socket
|> assign(:page_title, "New Site")
|> assign(:site, %Site{})
|> assign(:form, to_form(changeset))
|> assign(:org_agent, org_agent)
end
defp apply_action(socket, :edit, %{"id" => id}) do
@ -39,11 +48,20 @@ defmodule ToweropsWeb.SiteLive.Form do
available_parent_sites =
Enum.reject(socket.assigns.available_parent_sites, fn s -> s.id == site.id end)
# Get inherited agent info from organization
org_agent =
if socket.assigns.organization.default_agent_token_id do
Enum.find(socket.assigns.available_agents, fn a ->
a.id == socket.assigns.organization.default_agent_token_id
end)
end
socket
|> assign(:page_title, "Edit Site")
|> assign(:site, site)
|> assign(:form, to_form(changeset))
|> assign(:available_parent_sites, available_parent_sites)
|> assign(:org_agent, org_agent)
end
@impl true

View file

@ -48,9 +48,24 @@
prompt="Inherit from organization"
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Set a default agent for all equipment at this site. Equipment can override this setting individually.
</p>
<%= if @live_action == :edit and @site.agent_token_id do %>
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>Overriding</strong> organization default -
all equipment at this site will use this agent
</p>
<% else %>
<%= if Map.get(assigns, :org_agent) do %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Currently inheriting organization default: <strong>{@org_agent.name}</strong>
</p>
<% else %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Set a default agent for all equipment at this site. Equipment can override this setting individually.
</p>
<% end %>
<% end %>
<% end %>
<.input field={@form[:description]} type="textarea" label="Description" />

View file

@ -96,30 +96,26 @@
</div>
<% else %>
<div class="space-y-3">
<div
<.link
:for={eq <- @equipment}
class="flex items-center justify-between rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900"
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{eq.id}"}
class="flex items-center justify-between rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900 hover:border-zinc-300 dark:hover:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors cursor-pointer"
>
<div>
<p class="font-medium text-zinc-900 dark:text-zinc-100">{eq.name}</p>
<p class="text-sm text-zinc-600 dark:text-zinc-400">{eq.ip_address}</p>
</div>
<div class="flex items-center gap-3">
<span class={[
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
eq.status == :up &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
eq.status == :down && "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
eq.status == :unknown &&
"bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
]}>
{eq.status |> to_string() |> String.upcase()}
</span>
<.button navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{eq.id}"}>
View
</.button>
</div>
</div>
<span class={[
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
eq.status == :up &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
eq.status == :down && "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
eq.status == :unknown &&
"bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
]}>
{eq.status |> to_string() |> String.upcase()}
</span>
</.link>
</div>
<% end %>
</div>