- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat - Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast - ~11,600 color replacements across ~99 files - Update tests for new color class names
333 lines
13 KiB
Text
333 lines
13 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
|
>
|
|
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
|
|
<.breadcrumb items={[
|
|
%{label: "Dashboard", navigate: ~p"/dashboard"},
|
|
%{label: "Settings", navigate: ~p"/orgs/#{@organization.slug}/settings"},
|
|
%{label: "Integrations", navigate: ~p"/orgs/#{@organization.slug}/settings/integrations"},
|
|
%{label: "Preseem"}
|
|
]} />
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
|
|
{t("Network Insights")}
|
|
</h1>
|
|
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
|
|
{t("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-cool-steel-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-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-400",
|
|
else:
|
|
"text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-cool-steel-300"
|
|
)
|
|
]}
|
|
>
|
|
{t("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-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-400",
|
|
else:
|
|
"text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-cool-steel-300"
|
|
)
|
|
]}
|
|
>
|
|
{t("Dismissed")}
|
|
</.link>
|
|
</nav>
|
|
|
|
<%!-- Urgency filter --%>
|
|
<div class="flex items-center gap-2 border-l border-cool-steel-200 pl-4 dark:border-white/10">
|
|
<span class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-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-cool-steel-200 text-cool-steel-800 dark:bg-white/20 dark:text-white",
|
|
else:
|
|
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
|
|
)
|
|
]}
|
|
>
|
|
{t("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-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400",
|
|
else:
|
|
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
|
|
)
|
|
]}
|
|
>
|
|
{t("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-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400",
|
|
else:
|
|
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
|
|
)
|
|
]}
|
|
>
|
|
{t("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-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400",
|
|
else:
|
|
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
|
|
)
|
|
]}
|
|
>
|
|
{t("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-cerulean-50 px-4 py-3 dark:bg-cerulean-900/20"
|
|
>
|
|
<span class="text-sm font-medium text-cerulean-700 dark:text-cerulean-300">
|
|
{MapSet.size(@selected_ids)} selected
|
|
</span>
|
|
<button
|
|
type="button"
|
|
id="deselect-all-btn"
|
|
phx-click="deselect_all"
|
|
class="text-sm text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
|
|
>
|
|
{t("Deselect All")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
id="bulk-dismiss-btn"
|
|
phx-click="bulk_dismiss"
|
|
class="rounded-md bg-cerulean-600 px-3 py-1.5 text-sm font-medium text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
|
|
>
|
|
{t("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-cool-steel-300 p-12 text-center dark:border-cool-steel-700"
|
|
>
|
|
<.icon
|
|
name="hero-light-bulb"
|
|
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
|
|
/>
|
|
<h3 class="mt-2 text-sm font-semibold text-cool-steel-900 dark:text-white">
|
|
{t("No insights found")}
|
|
</h3>
|
|
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
|
|
<%= cond do %>
|
|
<% @filter_status == "dismissed" -> %>
|
|
{t("No dismissed insights.")}
|
|
<% @filter_urgency != nil -> %>
|
|
No {@filter_urgency} insights right now.
|
|
<% true -> %>
|
|
{t("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-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
|
|
>
|
|
{t("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-cool-steel-800/50",
|
|
if(selected?(@selected_ids, insight.id),
|
|
do:
|
|
"border-cerulean-300 ring-1 ring-cerulean-200 dark:border-cerulean-600 dark:ring-cerulean-800",
|
|
else: "border-cool-steel-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-cerulean-600 bg-cerulean-600 dark:border-cerulean-500 dark:bg-cerulean-500",
|
|
else:
|
|
"border-cool-steel-300 bg-white dark:border-cool-steel-600 dark:bg-cool-steel-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-cool-steel-100 px-2 py-0.5 text-xs font-medium text-cool-steel-600 dark:bg-cool-steel-700 dark:text-cool-steel-300">
|
|
{String.replace(insight.type, "_", " ")}
|
|
</span>
|
|
</div>
|
|
|
|
<h3 class="mt-1 text-sm font-semibold text-cool-steel-900 dark:text-white">
|
|
{insight.title}
|
|
</h3>
|
|
|
|
<%= if insight.description do %>
|
|
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
|
|
{insight.description}
|
|
</p>
|
|
<% end %>
|
|
|
|
<%= if insight.llm_summary do %>
|
|
<div class="mt-2 rounded-md border border-cerulean-200 bg-cerulean-50 px-3 py-2 text-sm text-cerulean-900 dark:border-cerulean-900/40 dark:bg-cerulean-950/40 dark:text-cerulean-100">
|
|
<p class="font-medium">{t("Summary")}</p>
|
|
<p>{insight.llm_summary}</p>
|
|
<%= if insight.recommended_action do %>
|
|
<p class="mt-1">
|
|
<span class="font-semibold">{t("Recommended action:")}</span>
|
|
{insight.recommended_action}
|
|
</p>
|
|
<% end %>
|
|
<p class="mt-1 text-xs text-cerulean-700 dark:text-cerulean-300">
|
|
AI-generated{if insight.llm_model, do: " · " <> insight.llm_model, else: ""}
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-cool-steel-500 dark:text-cool-steel-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-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-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" />
|
|
{ToweropsWeb.TimeHelpers.format_datetime(
|
|
insight.inserted_at,
|
|
@current_scope.timezone,
|
|
@current_scope.time_format
|
|
)}
|
|
</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-cool-steel-600 hover:bg-cool-steel-100 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-white"
|
|
>
|
|
{t("Dismiss")}
|
|
</button>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</Layouts.authenticated>
|