towerops/lib/towerops/search.ex
Graham McIntie 9e7ee5099d refactor: fix all credo strict issues, format all code, fix broken routes
- Fix broken route paths in dashboard (/discovery, /subscribers/trace, /config-changes)
- Fix insights empty state settings link (org-scoped route)
- Add underscores to all 86400 literals across 6 files
- Alphabetize aliases in search.ex and agent_channel.ex
- Extract changelog parser helper to reduce nesting
- Extract dashboard impact calculation to reduce nesting
- Refactor agent_channel config change detection (pattern match, extract function)
- Combine double Enum.reject into single call in trace.ex
- Fix line length in trace.ex search query
- Replace length/1 > 0 with != [] in trace_live
- Run mix format on all files
2026-02-13 19:34:40 -06:00

94 lines
2.8 KiB
Elixir

defmodule Towerops.Search do
@moduledoc """
Global search across devices, sites, accounts, and alerts.
"""
import Ecto.Query
alias Towerops.Alerts.Alert
alias Towerops.Devices.Device
alias Towerops.Gaiia.Account
alias Towerops.Repo
alias Towerops.Sites.Site
@limit 5
@doc """
Searches across multiple entity types for the given organization.
Returns a map of categorized results.
"""
def search(_organization_id, query) when byte_size(query) < 2, do: %{}
def search(organization_id, query) do
pattern = "%#{sanitize(query)}%"
%{
devices: search_devices(organization_id, pattern),
sites: search_sites(organization_id, pattern),
accounts: search_accounts(organization_id, pattern),
alerts: search_alerts(organization_id, pattern)
}
|> Enum.reject(fn {_k, v} -> v == [] end)
|> Map.new()
end
defp search_devices(organization_id, pattern) do
Device
|> where([d], d.organization_id == ^organization_id)
|> where([d], ilike(d.name, ^pattern) or ilike(d.ip_address, ^pattern))
|> order_by([d], d.name)
|> limit(@limit)
|> select([d], %{id: d.id, name: d.name, ip_address: d.ip_address})
|> Repo.all()
|> Enum.map(fn d ->
%{type: :device, label: d.name, sublabel: d.ip_address, url: "/devices/#{d.id}"}
end)
end
defp search_sites(organization_id, pattern) do
Site
|> where([s], s.organization_id == ^organization_id)
|> where([s], ilike(s.name, ^pattern))
|> order_by([s], s.name)
|> limit(@limit)
|> select([s], %{id: s.id, name: s.name, location: s.location})
|> Repo.all()
|> Enum.map(fn s ->
%{type: :site, label: s.name, sublabel: s.location, url: "/sites/#{s.id}"}
end)
end
defp search_accounts(organization_id, pattern) do
Account
|> where([a], a.organization_id == ^organization_id)
|> where([a], ilike(a.name, ^pattern) or ilike(a.readable_id, ^pattern))
|> order_by([a], a.name)
|> limit(@limit)
|> select([a], %{id: a.id, name: a.name, readable_id: a.readable_id})
|> Repo.all()
|> Enum.map(fn a ->
%{type: :account, label: a.name, sublabel: a.readable_id, url: "/trace?account_id=#{a.id}"}
end)
end
defp search_alerts(organization_id, pattern) do
Alert
|> join(:inner, [a], d in Device, on: a.device_id == d.id)
|> where([a, d], d.organization_id == ^organization_id)
|> where([a, _d], ilike(a.message, ^pattern))
|> where([a, _d], is_nil(a.resolved_at))
|> order_by([a, _d], desc: a.triggered_at)
|> limit(@limit)
|> select([a, d], %{id: a.id, message: a.message, device_name: d.name})
|> Repo.all()
|> Enum.map(fn a ->
%{type: :alert, label: a.message || "Alert", sublabel: a.device_name, url: "/alerts"}
end)
end
defp sanitize(query) do
query
|> String.replace("%", "\\%")
|> String.replace("_", "\\_")
end
end