620 lines
28 KiB
Text
620 lines
28 KiB
Text
<div class={[
|
|
"h-screen flex flex-col",
|
|
if(@fullscreen, do: "fixed inset-0 z-50 bg-white dark:bg-gray-900")
|
|
]}>
|
|
<%= if not @fullscreen do %>
|
|
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="weathermap"
|
|
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
|
>
|
|
<.header>
|
|
<span class="flex items-center gap-2">
|
|
<.icon name="hero-signal" class="h-6 w-6 text-green-600" />
|
|
{t("Network Weathermap")}
|
|
<span class="inline-flex items-center rounded-full bg-green-50 px-2 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10 dark:bg-green-400/10 dark:text-green-400 dark:ring-green-400/30">
|
|
{t("Live")}
|
|
</span>
|
|
</span>
|
|
<:subtitle>{t("Real-time network utilization visualization")}</:subtitle>
|
|
<:actions>
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
|
|
<span class="relative flex h-2 w-2">
|
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
|
</span>
|
|
<span>60s refresh</span>
|
|
</div>
|
|
<button
|
|
phx-click="toggle_fullscreen"
|
|
class="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
>
|
|
<.icon name="hero-arrows-pointing-out" class="h-4 w-4" />
|
|
{t("Fullscreen")}
|
|
</button>
|
|
</div>
|
|
</:actions>
|
|
</.header>
|
|
</Layouts.authenticated>
|
|
<% else %>
|
|
<!-- Fullscreen header -->
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900">
|
|
<div class="flex items-center gap-3">
|
|
<.icon name="hero-signal" class="h-6 w-6 text-green-600" />
|
|
<h1 class="text-xl font-semibold text-gray-900 dark:text-white">Network Weathermap</h1>
|
|
<span class="inline-flex items-center rounded-full bg-green-50 px-2 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10 dark:bg-green-400/10 dark:text-green-400 dark:ring-green-400/30">
|
|
Live
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
|
|
<span class="relative flex h-2 w-2">
|
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
|
</span>
|
|
<span>60s refresh</span>
|
|
</div>
|
|
<button
|
|
phx-click="toggle_fullscreen"
|
|
class="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
>
|
|
<.icon name="hero-arrows-pointing-in" class="h-4 w-4" />
|
|
{t("Exit Fullscreen")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Tab Navigation -->
|
|
<div class="border-b border-gray-200 dark:border-white/10 px-4 bg-white dark:bg-gray-900">
|
|
<nav class="-mb-px flex space-x-8">
|
|
<.link
|
|
patch={~p"/weathermap?tab=added#{if(@fullscreen, do: "&fullscreen=true", else: "")}"}
|
|
class={[
|
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
if @active_tab == "added" do
|
|
"border-blue-500 text-blue-600 dark:text-blue-400"
|
|
else
|
|
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
|
|
end
|
|
]}
|
|
>
|
|
{t("Added Devices")}
|
|
</.link>
|
|
|
|
<.link
|
|
patch={~p"/weathermap?tab=all#{if(@fullscreen, do: "&fullscreen=true", else: "")}"}
|
|
class={[
|
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
if @active_tab == "all" do
|
|
"border-blue-500 text-blue-600 dark:text-blue-400"
|
|
else
|
|
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
|
|
end
|
|
]}
|
|
>
|
|
{t("All Devices")}
|
|
<span class="ml-2 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-300">
|
|
{@topology.stats.discovered_devices}
|
|
</span>
|
|
</.link>
|
|
</nav>
|
|
</div>
|
|
|
|
<%= if @loading do %>
|
|
<div class="flex-1 flex items-center justify-center bg-white dark:bg-gray-900">
|
|
<div class="text-center">
|
|
<.icon name="hero-arrow-path" class="h-12 w-12 text-gray-400 animate-spin mx-auto" />
|
|
<p class="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading network weathermap...</p>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<!-- Stats Bar -->
|
|
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-6 p-4 bg-gray-50 dark:bg-gray-800/50">
|
|
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
|
|
<div class="p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<.icon name="hero-server" class="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 truncate">
|
|
{t("Devices")}
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
|
{@topology.stats.total_devices}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
|
|
<div class="p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<.icon name="hero-link" class="h-5 w-5 text-blue-400" />
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 truncate">
|
|
{t("Links")}
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
|
{@topology.stats.total_links}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
|
|
<div class="p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-5 h-5 rounded bg-green-400"></div>
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 truncate">
|
|
{t("Low (0-30%)")}
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
|
{@topology.stats.low_utilization_links || 0}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
|
|
<div class="p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-5 h-5 rounded bg-yellow-400"></div>
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 truncate">
|
|
{t("Medium (30-60%)")}
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
|
{@topology.stats.medium_utilization_links || 0}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
|
|
<div class="p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-5 h-5 rounded bg-orange-400"></div>
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 truncate">
|
|
{t("High (60-80%)")}
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
|
{@topology.stats.high_utilization_links || 0}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
|
|
<div class="p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-5 h-5 rounded bg-red-400"></div>
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 truncate">
|
|
{t("Critical (80%+)")}
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
|
{@topology.stats.overutilized_links || 0}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Bar -->
|
|
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-white/10 p-3">
|
|
<div class="flex items-center justify-between gap-4 flex-wrap">
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
phx-click="set_filter"
|
|
phx-value-filter="all"
|
|
class={[
|
|
"px-3 py-1.5 text-xs font-medium rounded-md transition-colors",
|
|
if(@filter == "all",
|
|
do: "bg-blue-100 text-blue-700 dark:bg-blue-400/20 dark:text-blue-400",
|
|
else: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
)
|
|
]}
|
|
>
|
|
All Links
|
|
</button>
|
|
<button
|
|
phx-click="set_filter"
|
|
phx-value-filter="high_util"
|
|
class={[
|
|
"px-3 py-1.5 text-xs font-medium rounded-md transition-colors",
|
|
if(@filter == "high_util",
|
|
do: "bg-orange-100 text-orange-700 dark:bg-orange-400/20 dark:text-orange-400",
|
|
else: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
)
|
|
]}
|
|
>
|
|
High Utilization (60%+)
|
|
<%= if (@topology.stats[:high_utilization_links] || 0) + (@topology.stats[:overutilized_links] || 0) > 0 do %>
|
|
<span class="ml-1 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-orange-800 bg-orange-200 rounded-full dark:bg-orange-900 dark:text-orange-300">
|
|
{(@topology.stats[:high_utilization_links] || 0) +
|
|
(@topology.stats[:overutilized_links] || 0)}
|
|
</span>
|
|
<% end %>
|
|
</button>
|
|
<button
|
|
phx-click="set_filter"
|
|
phx-value-filter="critical"
|
|
class={[
|
|
"px-3 py-1.5 text-xs font-medium rounded-md transition-colors",
|
|
if(@filter == "critical",
|
|
do: "bg-red-100 text-red-700 dark:bg-red-400/20 dark:text-red-400",
|
|
else: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
)
|
|
]}
|
|
>
|
|
Critical (80%+)
|
|
<%= if (@topology.stats[:overutilized_links] || 0) > 0 do %>
|
|
<span class="ml-1 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-red-800 bg-red-200 rounded-full dark:bg-red-900 dark:text-red-300">
|
|
{@topology.stats.overutilized_links}
|
|
</span>
|
|
<% end %>
|
|
</button>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="relative">
|
|
<.icon
|
|
name="hero-magnifying-glass"
|
|
class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400"
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Search devices..."
|
|
phx-keyup="search"
|
|
phx-key="Enter"
|
|
phx-debounce="300"
|
|
value={@search_query}
|
|
class="pl-8 pr-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 w-48"
|
|
/>
|
|
</div>
|
|
<%= if @topology[:has_geo] do %>
|
|
<div class="flex items-center gap-1 border-l border-gray-200 dark:border-gray-600 pl-3">
|
|
<button
|
|
phx-click="toggle_layout"
|
|
phx-value-mode="force"
|
|
class={[
|
|
"px-2 py-1 text-xs rounded transition-colors",
|
|
if(@layout_mode == "force",
|
|
do: "bg-gray-200 dark:bg-gray-600 text-gray-900 dark:text-white font-medium",
|
|
else: "text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
)
|
|
]}
|
|
title="Force-directed layout"
|
|
>
|
|
<.icon name="hero-share" class="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
phx-click="toggle_layout"
|
|
phx-value-mode="geo"
|
|
class={[
|
|
"px-2 py-1 text-xs rounded transition-colors",
|
|
if(@layout_mode == "geo",
|
|
do: "bg-gray-200 dark:bg-gray-600 text-gray-900 dark:text-white font-medium",
|
|
else: "text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
)
|
|
]}
|
|
title="Geographic layout"
|
|
>
|
|
<.icon name="hero-map" class="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Weathermap Container -->
|
|
<div class="flex-1 bg-white dark:bg-gray-800 relative overflow-hidden">
|
|
<div class="absolute inset-0 border-t border-gray-200 dark:border-white/10">
|
|
<div
|
|
id="cy-weathermap-container"
|
|
phx-hook="WeathermapViewer"
|
|
phx-update="ignore"
|
|
class="w-full h-full"
|
|
data-topology={Jason.encode!(@topology)}
|
|
>
|
|
</div>
|
|
|
|
<!-- Zoom Controls -->
|
|
<div class="absolute bottom-4 left-4 flex flex-col gap-1 z-10">
|
|
<button
|
|
id="cy-zoom-in"
|
|
type="button"
|
|
title="Zoom in"
|
|
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-gray-700 border border-gray-200 dark:border-white/10 rounded shadow-sm text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white transition-colors"
|
|
>
|
|
<.icon name="hero-plus" class="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
id="cy-zoom-out"
|
|
type="button"
|
|
title="Zoom out"
|
|
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-gray-700 border border-gray-200 dark:border-white/10 rounded shadow-sm text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white transition-colors"
|
|
>
|
|
<.icon name="hero-minus" class="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
id="cy-fit"
|
|
type="button"
|
|
title="Fit to screen"
|
|
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-gray-700 border border-gray-200 dark:border-white/10 rounded shadow-sm text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white transition-colors"
|
|
>
|
|
<.icon name="hero-arrows-pointing-out" class="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Utilization Legend -->
|
|
<div class="absolute bottom-4 right-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-white/10 rounded-lg shadow-lg p-4 z-10">
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-3">Link Utilization</h4>
|
|
<div class="space-y-2 text-xs">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-6 h-1 rounded bg-green-500"></div>
|
|
<span class="text-gray-700 dark:text-gray-300">0-30% (Good)</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-6 h-1 rounded bg-yellow-500"></div>
|
|
<span class="text-gray-700 dark:text-gray-300">30-60% (Moderate)</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-6 h-1 rounded bg-orange-500"></div>
|
|
<span class="text-gray-700 dark:text-gray-300">60-80% (High)</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-6 h-1 rounded bg-red-500"></div>
|
|
<span class="text-gray-700 dark:text-gray-300">80%+ (Critical)</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-6 h-1 border border-gray-300 dark:border-gray-600"></div>
|
|
<span class="text-gray-500 dark:text-gray-400">No capacity data</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-white/10 text-xs text-gray-500 dark:text-gray-400">
|
|
Last updated: <.timestamp datetime={@topology.last_updated} timezone={@timezone} />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Node Detail Panel (slide-out) -->
|
|
<%= if @selected_node_detail do %>
|
|
<div
|
|
id="node-detail-panel"
|
|
class="absolute top-0 right-0 h-full w-80 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-white/10 shadow-lg overflow-y-auto transition-transform duration-200"
|
|
>
|
|
<div class="p-4">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("Node Details")}
|
|
</h3>
|
|
<button
|
|
phx-click="close_detail_panel"
|
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
<.icon name="hero-x-mark" class="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Device Info -->
|
|
<div class="space-y-3">
|
|
<div>
|
|
<h4 class="text-base font-medium text-gray-900 dark:text-white">
|
|
{@selected_node_detail.name}
|
|
</h4>
|
|
<%= if @selected_node_detail.type == :discovered do %>
|
|
<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 mt-1">
|
|
{t("Discovered")}
|
|
</span>
|
|
<% end %>
|
|
</div>
|
|
|
|
<%= if @selected_node_detail.ip_address do %>
|
|
<div class="flex items-center text-sm">
|
|
<.icon name="hero-globe-alt" class="h-4 w-4 text-gray-400 mr-2 flex-shrink-0" />
|
|
<span class="text-gray-700 dark:text-gray-300">
|
|
{@selected_node_detail.ip_address}
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%= if @selected_node_detail.site_name do %>
|
|
<div class="flex items-center text-sm">
|
|
<.icon name="hero-map-pin" class="h-4 w-4 text-gray-400 mr-2 flex-shrink-0" />
|
|
<span class="text-gray-700 dark:text-gray-300">
|
|
{@selected_node_detail.site_name}
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%= if @selected_node_detail.manufacturer do %>
|
|
<div class="flex items-center text-sm">
|
|
<.icon
|
|
name="hero-building-office"
|
|
class="h-4 w-4 text-gray-400 mr-2 flex-shrink-0"
|
|
/>
|
|
<span class="text-gray-700 dark:text-gray-300">
|
|
{@selected_node_detail.manufacturer}
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="flex items-center text-sm">
|
|
<.icon name="hero-tag" class="h-4 w-4 text-gray-400 mr-2 flex-shrink-0" />
|
|
<span class="text-gray-700 dark:text-gray-300 capitalize">
|
|
{@selected_node_detail.role}
|
|
</span>
|
|
</div>
|
|
|
|
<%= if @selected_node_detail.status && @selected_node_detail.status != :unknown do %>
|
|
<div class="flex items-center text-sm">
|
|
<span class={[
|
|
"inline-block h-2 w-2 rounded-full mr-2",
|
|
if(@selected_node_detail.status == :up,
|
|
do: "bg-green-400",
|
|
else: "bg-red-400"
|
|
)
|
|
]}></span>
|
|
<span class="text-gray-700 dark:text-gray-300 capitalize">
|
|
{@selected_node_detail.status}
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<!-- Utilization Stats (for devices with bandwidth data) -->
|
|
<%= if @selected_node_detail[:utilization_stats] do %>
|
|
<div class="mt-4 pt-3 border-t border-gray-200 dark:border-white/10">
|
|
<h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
|
|
Interface Utilization
|
|
</h4>
|
|
<div class="space-y-2">
|
|
<%= for stat <- @selected_node_detail.utilization_stats do %>
|
|
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-md p-3">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<div class="text-xs font-medium text-gray-900 dark:text-white">
|
|
{stat.interface_name}
|
|
</div>
|
|
<div class={[
|
|
"text-xs font-semibold",
|
|
case stat.utilization_level do
|
|
:low -> "text-green-600 dark:text-green-400"
|
|
:medium -> "text-yellow-600 dark:text-yellow-400"
|
|
:high -> "text-orange-600 dark:text-orange-400"
|
|
:critical -> "text-red-600 dark:text-red-400"
|
|
_ -> "text-gray-500 dark:text-gray-400"
|
|
end
|
|
]}>
|
|
{stat.utilization_pct}%
|
|
</div>
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
{stat.throughput_text} / {stat.capacity_text}
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Connections -->
|
|
<%= if length(@selected_node_detail.connections) > 0 do %>
|
|
<div class="mt-5 pt-4 border-t border-gray-200 dark:border-white/10">
|
|
<h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
|
|
{t("Connections")} ({length(@selected_node_detail.connections)})
|
|
</h4>
|
|
<div class="space-y-2">
|
|
<%= for conn <- @selected_node_detail.connections do %>
|
|
<div class="flex items-center justify-between py-1.5 px-2 rounded bg-gray-50 dark:bg-gray-700/50 text-sm">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-gray-900 dark:text-white truncate font-medium text-xs">
|
|
{conn.device_name}
|
|
</p>
|
|
<%= if conn.interface do %>
|
|
<p class="text-gray-500 dark:text-gray-400 text-xs">
|
|
{conn.interface}
|
|
</p>
|
|
<% end %>
|
|
<%= if conn[:utilization_text] do %>
|
|
<p class="text-xs text-blue-600 dark:text-blue-400 mt-1">
|
|
{conn.utilization_text}
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
<div class="flex items-center space-x-2 ml-2 flex-shrink-0">
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">
|
|
{conn.link_type}
|
|
</span>
|
|
<span class={[
|
|
"text-xs font-medium",
|
|
cond do
|
|
conn.confidence > 0.9 -> "text-green-600 dark:text-green-400"
|
|
conn.confidence >= 0.5 -> "text-yellow-600 dark:text-yellow-400"
|
|
true -> "text-gray-500 dark:text-gray-400"
|
|
end
|
|
]}>
|
|
{trunc(conn.confidence * 100)}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- View Device Link (managed only) -->
|
|
<%= if @selected_node_detail.type == :managed do %>
|
|
<div class="mt-4">
|
|
<.link
|
|
navigate={~p"/devices/#{@selected_node_detail.id}"}
|
|
class="flex items-center justify-center w-full px-3 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-400/10 rounded-md hover:bg-blue-100 dark:hover:bg-blue-400/20"
|
|
>
|
|
<.icon name="hero-arrow-top-right-on-square" class="h-4 w-4 mr-1.5" />
|
|
{t("View Device")}
|
|
</.link>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<%= if @topology.stats.total_devices == 0 do %>
|
|
<div class="flex-1 flex items-center justify-center">
|
|
<div class="text-center py-16">
|
|
<.icon name="hero-signal" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
|
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t("No network data available")}
|
|
</h3>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{t(
|
|
"Add devices with SNMP enabled and configure interface capacities to view utilization."
|
|
)}
|
|
</p>
|
|
<div class="mt-6">
|
|
<.button navigate={~p"/devices/new"} variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> Add Your First Device
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|