Adds 7 new composable Query modules following the existing DeviceQuery/AlertQuery pattern. Each exposes base/0 and for_organization/1,2 plus filters used by 2+ callers. New modules: - Towerops.OnCall.ScheduleQuery - Towerops.OnCall.EscalationPolicyQuery - Towerops.Maintenance.MaintenanceWindowQuery - Towerops.Organizations.MembershipQuery - Towerops.Organizations.InvitationQuery - Towerops.Agents.AgentTokenQuery - Towerops.Gaiia.DeviceSubscriberLinkQuery Refactors callsites in alerts, agents, devices, gaiia, maintenance, on_call, organizations, search, and subscription_limits to use existing or new Query modules instead of inline 'where: x.organization_id == ^...' clauses.
95 lines
2.9 KiB
Elixir
95 lines
2.9 KiB
Elixir
defmodule Towerops.Search do
|
|
@moduledoc """
|
|
Global search across devices, sites, accounts, and alerts.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Alerts.AlertQuery
|
|
alias Towerops.Devices.Device
|
|
alias Towerops.Devices.DeviceQuery
|
|
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
|
|
organization_id
|
|
|> DeviceQuery.for_organization()
|
|
|> 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
|
|
organization_id
|
|
|> AlertQuery.for_organization()
|
|
|> join(:inner, [a], d in Device, on: a.device_id == d.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
|