add network insights feed page with filters and bulk dismiss
This commit is contained in:
parent
62b8ec7afd
commit
9ccea8daaf
5 changed files with 701 additions and 0 deletions
|
|
@ -85,6 +85,14 @@
|
|||
>
|
||||
Manage Devices →
|
||||
</.link>
|
||||
<.link
|
||||
navigate={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/insights"
|
||||
}
|
||||
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Network Insights →
|
||||
</.link>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
163
lib/towerops_web/live/org/preseem_insights_live.ex
Normal file
163
lib/towerops_web/live/org/preseem_insights_live.ex
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
defmodule ToweropsWeb.Org.PreseemInsightsLive do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :live_view
|
||||
|
||||
alias Towerops.Preseem
|
||||
alias Towerops.Repo
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
org = socket.assigns.current_scope.organization
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:organization, org)
|
||||
|> assign(:selected_ids, MapSet.new())}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
filter_type = params["type"]
|
||||
filter_urgency = params["urgency"]
|
||||
filter_status = params["status"] || "active"
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:filter_type, filter_type)
|
||||
|> assign(:filter_urgency, filter_urgency)
|
||||
|> assign(:filter_status, filter_status)
|
||||
|> assign(:selected_ids, MapSet.new())
|
||||
|> load_insights()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("filter", params, socket) do
|
||||
org = socket.assigns.organization
|
||||
|
||||
query_params = %{}
|
||||
|
||||
query_params =
|
||||
if params["type"] && params["type"] != "",
|
||||
do: Map.put(query_params, "type", params["type"]),
|
||||
else: query_params
|
||||
|
||||
query_params =
|
||||
if params["urgency"] && params["urgency"] != "",
|
||||
do: Map.put(query_params, "urgency", params["urgency"]),
|
||||
else: query_params
|
||||
|
||||
query_params =
|
||||
if params["status"] && params["status"] != "",
|
||||
do: Map.put(query_params, "status", params["status"]),
|
||||
else: query_params
|
||||
|
||||
{:noreply,
|
||||
push_patch(socket,
|
||||
to: ~p"/orgs/#{org.slug}/settings/integrations/preseem/insights?#{query_params}"
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("dismiss", %{"id" => id}, socket) do
|
||||
case Preseem.dismiss_insight(id) do
|
||||
{:ok, _} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> load_insights()
|
||||
|> put_flash(:info, "Insight dismissed")}
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to dismiss")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_select", %{"id" => id}, socket) do
|
||||
selected = socket.assigns.selected_ids
|
||||
|
||||
selected =
|
||||
if MapSet.member?(selected, id),
|
||||
do: MapSet.delete(selected, id),
|
||||
else: MapSet.put(selected, id)
|
||||
|
||||
{:noreply, assign(socket, :selected_ids, selected)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("select_all", _params, socket) do
|
||||
ids = MapSet.new(socket.assigns.insights, & &1.id)
|
||||
{:noreply, assign(socket, :selected_ids, ids)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("deselect_all", _params, socket) do
|
||||
{:noreply, assign(socket, :selected_ids, MapSet.new())}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("bulk_dismiss", _params, socket) do
|
||||
ids = MapSet.to_list(socket.assigns.selected_ids)
|
||||
|
||||
if ids == [] do
|
||||
{:noreply, socket}
|
||||
else
|
||||
case Preseem.dismiss_insights(ids) do
|
||||
{:ok, count} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:selected_ids, MapSet.new())
|
||||
|> load_insights()
|
||||
|> put_flash(:info, "#{count} insight(s) dismissed")}
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to dismiss insights")}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp load_insights(socket) do
|
||||
org_id = socket.assigns.organization.id
|
||||
opts = [status: socket.assigns.filter_status]
|
||||
|
||||
opts =
|
||||
if socket.assigns.filter_type,
|
||||
do: Keyword.put(opts, :type, socket.assigns.filter_type),
|
||||
else: opts
|
||||
|
||||
opts =
|
||||
if socket.assigns.filter_urgency,
|
||||
do: Keyword.put(opts, :urgency, socket.assigns.filter_urgency),
|
||||
else: opts
|
||||
|
||||
insights =
|
||||
org_id
|
||||
|> Preseem.list_insights(opts)
|
||||
|> Repo.preload([:preseem_access_point, :device])
|
||||
|
||||
assign(socket, :insights, insights)
|
||||
end
|
||||
|
||||
defp urgency_classes(urgency) do
|
||||
case urgency do
|
||||
"critical" ->
|
||||
"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
|
||||
|
||||
"warning" ->
|
||||
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
|
||||
"info" ->
|
||||
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
|
||||
_ ->
|
||||
"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
end
|
||||
end
|
||||
|
||||
defp selected?(selected_ids, id) do
|
||||
MapSet.member?(selected_ids, id)
|
||||
end
|
||||
|
||||
defp any_selected?(selected_ids) do
|
||||
MapSet.size(selected_ids) > 0
|
||||
end
|
||||
end
|
||||
303
lib/towerops_web/live/org/preseem_insights_live.html.heex
Normal file
303
lib/towerops_web/live/org/preseem_insights_live.html.heex
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
<Layouts.authenticated
|
||||
flash={@flash}
|
||||
current_scope={@current_scope}
|
||||
>
|
||||
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
|
||||
<div class="mb-4">
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@organization.slug}/settings/integrations"}
|
||||
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
>
|
||||
<.icon name="hero-arrow-left" class="h-4 w-4" /> Back to Integrations
|
||||
</.link>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
|
||||
Network Insights
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Proactive network health observations generated from Preseem data analysis.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Filter bar --%>
|
||||
<div class="mt-6 flex flex-wrap items-center gap-4 border-b border-gray-200 pb-4 dark:border-white/10">
|
||||
<%!-- Status filter tabs --%>
|
||||
<nav class="flex space-x-4">
|
||||
<.link
|
||||
id="filter-active"
|
||||
patch={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/insights?status=active"
|
||||
}
|
||||
class={[
|
||||
"rounded-md px-3 py-1.5 text-sm font-medium",
|
||||
if(@filter_status == "active",
|
||||
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400",
|
||||
else:
|
||||
"text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-300"
|
||||
)
|
||||
]}
|
||||
>
|
||||
Active
|
||||
</.link>
|
||||
<.link
|
||||
id="filter-dismissed"
|
||||
patch={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/insights?status=dismissed"
|
||||
}
|
||||
class={[
|
||||
"rounded-md px-3 py-1.5 text-sm font-medium",
|
||||
if(@filter_status == "dismissed",
|
||||
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400",
|
||||
else:
|
||||
"text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-300"
|
||||
)
|
||||
]}
|
||||
>
|
||||
Dismissed
|
||||
</.link>
|
||||
</nav>
|
||||
|
||||
<%!-- Urgency filter --%>
|
||||
<div class="flex items-center gap-2 border-l border-gray-200 pl-4 dark:border-white/10">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">Urgency:</span>
|
||||
<.link
|
||||
id="filter-urgency-all"
|
||||
patch={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/insights?#{%{status: @filter_status}}"
|
||||
}
|
||||
class={[
|
||||
"rounded-md px-2 py-1 text-xs font-medium",
|
||||
if(is_nil(@filter_urgency),
|
||||
do: "bg-gray-200 text-gray-800 dark:bg-white/20 dark:text-white",
|
||||
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
|
||||
)
|
||||
]}
|
||||
>
|
||||
All
|
||||
</.link>
|
||||
<.link
|
||||
id="filter-urgency-critical"
|
||||
patch={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/insights?#{%{status: @filter_status, urgency: "critical"}}"
|
||||
}
|
||||
class={[
|
||||
"rounded-md px-2 py-1 text-xs font-medium",
|
||||
if(@filter_urgency == "critical",
|
||||
do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
|
||||
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
|
||||
)
|
||||
]}
|
||||
>
|
||||
Critical
|
||||
</.link>
|
||||
<.link
|
||||
id="filter-urgency-warning"
|
||||
patch={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/insights?#{%{status: @filter_status, urgency: "warning"}}"
|
||||
}
|
||||
class={[
|
||||
"rounded-md px-2 py-1 text-xs font-medium",
|
||||
if(@filter_urgency == "warning",
|
||||
do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
|
||||
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
|
||||
)
|
||||
]}
|
||||
>
|
||||
Warning
|
||||
</.link>
|
||||
<.link
|
||||
id="filter-urgency-info"
|
||||
patch={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/insights?#{%{status: @filter_status, urgency: "info"}}"
|
||||
}
|
||||
class={[
|
||||
"rounded-md px-2 py-1 text-xs font-medium",
|
||||
if(@filter_urgency == "info",
|
||||
do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
||||
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
|
||||
)
|
||||
]}
|
||||
>
|
||||
Info
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Bulk actions bar --%>
|
||||
<%= if any_selected?(@selected_ids) do %>
|
||||
<div
|
||||
id="bulk-actions"
|
||||
class="mt-4 flex items-center gap-3 rounded-lg bg-indigo-50 px-4 py-3 dark:bg-indigo-900/20"
|
||||
>
|
||||
<span class="text-sm font-medium text-indigo-700 dark:text-indigo-300">
|
||||
{MapSet.size(@selected_ids)} selected
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
id="deselect-all-btn"
|
||||
phx-click="deselect_all"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||
>
|
||||
Deselect All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="bulk-dismiss-btn"
|
||||
phx-click="bulk_dismiss"
|
||||
class="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
||||
>
|
||||
Dismiss Selected
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%!-- Insights list --%>
|
||||
<div class="mt-6" id="insights-list">
|
||||
<%= if @insights == [] do %>
|
||||
<div
|
||||
id="empty-state"
|
||||
class="rounded-lg border-2 border-dashed border-gray-300 p-12 text-center dark:border-gray-700"
|
||||
>
|
||||
<.icon
|
||||
name="hero-light-bulb"
|
||||
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
No insights found
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= cond do %>
|
||||
<% @filter_status == "dismissed" -> %>
|
||||
No dismissed insights.
|
||||
<% @filter_urgency != nil -> %>
|
||||
No {@filter_urgency} insights right now.
|
||||
<% true -> %>
|
||||
No active insights. Your network is looking healthy!
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<%!-- Select all bar --%>
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
id="select-all-btn"
|
||||
phx-click="select_all"
|
||||
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
:for={insight <- @insights}
|
||||
id={"insight-#{insight.id}"}
|
||||
class={[
|
||||
"rounded-lg border bg-white p-4 transition-shadow hover:shadow-md dark:bg-gray-800/50",
|
||||
if(selected?(@selected_ids, insight.id),
|
||||
do:
|
||||
"border-indigo-300 ring-1 ring-indigo-200 dark:border-indigo-600 dark:ring-indigo-800",
|
||||
else: "border-gray-200 dark:border-white/10"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<%!-- Checkbox --%>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="toggle_select"
|
||||
phx-value-id={insight.id}
|
||||
class="mt-0.5 flex-shrink-0"
|
||||
>
|
||||
<div class={[
|
||||
"flex h-5 w-5 items-center justify-center rounded border",
|
||||
if(selected?(@selected_ids, insight.id),
|
||||
do:
|
||||
"border-indigo-600 bg-indigo-600 dark:border-indigo-500 dark:bg-indigo-500",
|
||||
else: "border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-700"
|
||||
)
|
||||
]}>
|
||||
<%= if selected?(@selected_ids, insight.id) do %>
|
||||
<.icon name="hero-check" class="h-3.5 w-3.5 text-white" />
|
||||
<% end %>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<%!-- Urgency badge --%>
|
||||
<span class={[
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
urgency_classes(insight.urgency)
|
||||
]}>
|
||||
{insight.urgency}
|
||||
</span>
|
||||
|
||||
<%!-- Type badge --%>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
{String.replace(insight.type, "_", " ")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-1 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{insight.title}
|
||||
</h3>
|
||||
|
||||
<%= if insight.description do %>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{insight.description}
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<%!-- Access point --%>
|
||||
<%= if insight.preseem_access_point do %>
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-signal" class="h-3.5 w-3.5" />
|
||||
{insight.preseem_access_point.name}
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%!-- Linked device --%>
|
||||
<%= if insight.device do %>
|
||||
<.link
|
||||
navigate={~p"/devices/#{insight.device.id}"}
|
||||
class="flex items-center gap-1 text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||
>
|
||||
<.icon name="hero-server" class="h-3.5 w-3.5" />
|
||||
{insight.device.name || "Device"}
|
||||
</.link>
|
||||
<% end %>
|
||||
|
||||
<%!-- Timestamp --%>
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-clock" class="h-3.5 w-3.5" />
|
||||
{Calendar.strftime(insight.inserted_at, "%Y-%m-%d %H:%M UTC")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Dismiss button --%>
|
||||
<%= if insight.status == "active" do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="dismiss"
|
||||
phx-value-id={insight.id}
|
||||
class="flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</Layouts.authenticated>
|
||||
|
|
@ -311,6 +311,7 @@ defmodule ToweropsWeb.Router do
|
|||
live "/settings", Org.SettingsLive, :index
|
||||
live "/settings/integrations", Org.IntegrationsLive, :index
|
||||
live "/settings/integrations/preseem/devices", Org.PreseemDevicesLive, :index
|
||||
live "/settings/integrations/preseem/insights", Org.PreseemInsightsLive, :index
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
226
test/towerops_web/live/org/preseem_insights_live_test.exs
Normal file
226
test/towerops_web/live/org/preseem_insights_live_test.exs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
defmodule ToweropsWeb.Org.PreseemInsightsLiveTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.DevicesFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.Preseem.AccessPoint
|
||||
alias Towerops.Preseem.Insight
|
||||
alias Towerops.Repo
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
%{user: user, organization: org}
|
||||
end
|
||||
|
||||
defp insert_access_point!(org, attrs \\ %{}) do
|
||||
default = %{
|
||||
organization_id: org.id,
|
||||
preseem_id: "ap-#{System.unique_integer([:positive])}",
|
||||
name: "Test AP"
|
||||
}
|
||||
|
||||
{:ok, ap} =
|
||||
%AccessPoint{}
|
||||
|> AccessPoint.changeset(Map.merge(default, attrs))
|
||||
|> Repo.insert()
|
||||
|
||||
ap
|
||||
end
|
||||
|
||||
defp insert_insight!(org, ap, attrs) do
|
||||
default = %{
|
||||
organization_id: org.id,
|
||||
preseem_access_point_id: ap.id,
|
||||
type: "qoe_degradation",
|
||||
urgency: "warning",
|
||||
channel: "proactive",
|
||||
title: "Test insight",
|
||||
description: "Test description"
|
||||
}
|
||||
|
||||
{:ok, insight} =
|
||||
%Insight{}
|
||||
|> Insight.changeset(Map.merge(default, attrs))
|
||||
|> Repo.insert()
|
||||
|
||||
insight
|
||||
end
|
||||
|
||||
describe "index" do
|
||||
test "renders empty state when no insights exist", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: org
|
||||
} do
|
||||
{:ok, view, html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights")
|
||||
|
||||
assert html =~ "Network Insights"
|
||||
assert has_element?(view, "#empty-state")
|
||||
end
|
||||
|
||||
test "shows insights in list", %{conn: conn, user: user, organization: org} do
|
||||
ap = insert_access_point!(org, %{name: "Tower 1"})
|
||||
insert_insight!(org, ap, %{title: "QoE dropping on Tower 1"})
|
||||
|
||||
{:ok, _view, html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights")
|
||||
|
||||
assert html =~ "QoE dropping on Tower 1"
|
||||
end
|
||||
|
||||
test "shows urgency badges", %{conn: conn, user: user, organization: org} do
|
||||
ap = insert_access_point!(org)
|
||||
|
||||
insert_insight!(org, ap, %{
|
||||
title: "Critical Issue",
|
||||
urgency: "critical"
|
||||
})
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights")
|
||||
|
||||
assert has_element?(view, "#insights-list")
|
||||
assert render(view) =~ "Critical Issue"
|
||||
assert render(view) =~ "critical"
|
||||
end
|
||||
|
||||
test "shows related access point name", %{conn: conn, user: user, organization: org} do
|
||||
ap = insert_access_point!(org, %{name: "Main Tower AP"})
|
||||
insert_insight!(org, ap, %{title: "Some insight"})
|
||||
|
||||
{:ok, _view, html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights")
|
||||
|
||||
assert html =~ "Main Tower AP"
|
||||
end
|
||||
|
||||
test "links to device page when device_id exists", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
organization: org
|
||||
} do
|
||||
ap = insert_access_point!(org)
|
||||
device = device_fixture(%{organization_id: org.id, name: "My Router"})
|
||||
|
||||
insert_insight!(org, ap, %{
|
||||
title: "Device issue",
|
||||
device_id: device.id
|
||||
})
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights")
|
||||
|
||||
assert has_element?(view, "a[href='/devices/#{device.id}']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "filtering" do
|
||||
setup %{organization: org} do
|
||||
ap = insert_access_point!(org)
|
||||
|
||||
critical =
|
||||
insert_insight!(org, ap, %{
|
||||
title: "Critical Issue",
|
||||
urgency: "critical"
|
||||
})
|
||||
|
||||
info =
|
||||
insert_insight!(org, ap, %{
|
||||
title: "Info Note",
|
||||
urgency: "info"
|
||||
})
|
||||
|
||||
%{ap: ap, critical: critical, info: info}
|
||||
end
|
||||
|
||||
test "filters by urgency via URL params", %{conn: conn, user: user, organization: org} do
|
||||
{:ok, _view, html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights?urgency=critical")
|
||||
|
||||
assert html =~ "Critical Issue"
|
||||
refute html =~ "Info Note"
|
||||
end
|
||||
|
||||
test "filters by status", %{conn: conn, user: user, organization: org, ap: ap} do
|
||||
insert_insight!(org, ap, %{
|
||||
title: "Dismissed One",
|
||||
status: "dismissed",
|
||||
dismissed_at: DateTime.truncate(DateTime.utc_now(), :second)
|
||||
})
|
||||
|
||||
{:ok, _view, html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights?status=dismissed")
|
||||
|
||||
assert html =~ "Dismissed One"
|
||||
refute html =~ "Critical Issue"
|
||||
end
|
||||
end
|
||||
|
||||
describe "dismiss" do
|
||||
test "dismisses a single insight", %{conn: conn, user: user, organization: org} do
|
||||
ap = insert_access_point!(org)
|
||||
insight = insert_insight!(org, ap, %{title: "Dismissable"})
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights")
|
||||
|
||||
assert render(view) =~ "Dismissable"
|
||||
|
||||
view
|
||||
|> element("button[phx-click=dismiss][phx-value-id='#{insight.id}']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Insight dismissed"
|
||||
refute html =~ "Dismissable"
|
||||
end
|
||||
end
|
||||
|
||||
describe "bulk actions" do
|
||||
test "select all and bulk dismiss", %{conn: conn, user: user, organization: org} do
|
||||
ap = insert_access_point!(org)
|
||||
insert_insight!(org, ap, %{title: "Bulk 1"})
|
||||
insert_insight!(org, ap, %{title: "Bulk 2"})
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings/integrations/preseem/insights")
|
||||
|
||||
assert render(view) =~ "Bulk 1"
|
||||
assert render(view) =~ "Bulk 2"
|
||||
|
||||
# Select all
|
||||
view |> element("#select-all-btn") |> render_click()
|
||||
|
||||
# Bulk dismiss
|
||||
view |> element("#bulk-dismiss-btn") |> render_click()
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "dismissed"
|
||||
refute html =~ "Bulk 1"
|
||||
refute html =~ "Bulk 2"
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue