Two issues were preventing impersonation from working correctly:
1. Templates were not passing current_scope to Layouts.authenticated,
so the impersonation banner never displayed.
2. fetch_current_scope_for_user was looking up the user from the
session token, which always pointed to the superuser. This caused
the superuser to see their own organizations and equipment instead
of the impersonated user's data.
Changes:
- Pass current_scope={@current_scope} to all Layouts.authenticated calls
- Store target_user_id in session during impersonation
- Fetch target user directly from database using target_user_id
- Update both fetch_current_scope_for_user (for controllers) and
mount_current_scope (for LiveViews) to properly handle impersonation
- Clean up target_user_id from session when impersonation ends
Now when a superuser impersonates a user, they correctly see:
- The impersonation banner at the top with exit link
- The target user's organizations and equipment
- All data scoped to the impersonated user
528 lines
25 KiB
Text
528 lines
25 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
current_organization={@current_organization}
|
|
active_page="equipment"
|
|
>
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{@equipment.name}</h1>
|
|
<p class="text-sm text-zinc-600 dark:text-zinc-400 font-mono">{@equipment.ip_address}</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<.button navigate={
|
|
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/edit"
|
|
}>
|
|
<.icon name="hero-pencil" class="h-4 w-4" /> Edit
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
<!-- Tabs -->
|
|
<div class="border-b border-zinc-200 dark:border-zinc-700">
|
|
<nav class="-mb-px flex space-x-8">
|
|
<.link
|
|
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=overview"}
|
|
class={[
|
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
if @active_tab == "overview" do
|
|
"border-blue-500 text-blue-600 dark:text-blue-400"
|
|
else
|
|
"border-transparent text-zinc-500 hover:text-zinc-700 hover:border-zinc-300 dark:text-zinc-400 dark:hover:text-zinc-300"
|
|
end
|
|
]}
|
|
>
|
|
Overview
|
|
</.link>
|
|
|
|
<%= if @snmp_device do %>
|
|
<.link
|
|
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=ports"}
|
|
class={[
|
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
if @active_tab == "ports" do
|
|
"border-blue-500 text-blue-600 dark:text-blue-400"
|
|
else
|
|
"border-transparent text-zinc-500 hover:text-zinc-700 hover:border-zinc-300 dark:text-zinc-400 dark:hover:text-zinc-300"
|
|
end
|
|
]}
|
|
>
|
|
Ports
|
|
</.link>
|
|
<% end %>
|
|
|
|
<.link
|
|
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=logs"}
|
|
class={[
|
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
if @active_tab == "logs" do
|
|
"border-blue-500 text-blue-600 dark:text-blue-400"
|
|
else
|
|
"border-transparent text-zinc-500 hover:text-zinc-700 hover:border-zinc-300 dark:text-zinc-400 dark:hover:text-zinc-300"
|
|
end
|
|
]}
|
|
>
|
|
Logs
|
|
</.link>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
<!-- Tab Content -->
|
|
<div class="mt-6">
|
|
<%= case @active_tab do %>
|
|
<% "overview" -> %>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
|
|
<!-- Left Column: Device Information & Traffic -->
|
|
<div class="space-y-6">
|
|
<!-- Device Information -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
Device Information
|
|
</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<dl class="space-y-3">
|
|
<%= if @snmp_device do %>
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">System Name</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{@snmp_device.sys_name || @equipment.name}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Resolved IP</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100 font-mono">
|
|
{@equipment.ip_address}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Hardware</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{@snmp_device.model || "N/A"}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Operating System</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{if @snmp_device.manufacturer && @snmp_device.firmware_version do
|
|
"#{@snmp_device.manufacturer} #{@snmp_device.firmware_version}"
|
|
else
|
|
@snmp_device.sys_descr || "N/A"
|
|
end}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Object ID</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100 font-mono">
|
|
{@snmp_device.sys_object_id || "N/A"}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Contact</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{@snmp_device.sys_contact || "N/A"}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Location</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{@snmp_device.sys_location || "N/A"}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Uptime</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{format_uptime(@snmp_device.sys_uptime)}
|
|
</dd>
|
|
</div>
|
|
<% else %>
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Name</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{@equipment.name}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">IP Address</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100 font-mono">
|
|
{@equipment.ip_address}
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Device Added</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{format_device_age(@equipment.inserted_at)}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="flex justify-between py-2">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Last Discovered</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{if @equipment.last_discovery_at do
|
|
format_device_age(@equipment.last_discovery_at)
|
|
else
|
|
"Never"
|
|
end}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
<!-- Overall Traffic Chart -->
|
|
<%= if @traffic_chart_data do %>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<.link
|
|
navigate={
|
|
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/traffic"
|
|
}
|
|
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
Overall Traffic
|
|
</h3>
|
|
<.icon name="hero-arrow-right" class="h-4 w-4 text-zinc-400" />
|
|
</div>
|
|
</.link>
|
|
<div class="p-4">
|
|
<div
|
|
id="traffic-chart"
|
|
phx-hook="SensorChart"
|
|
data-chart={@traffic_chart_data}
|
|
data-unit="bps"
|
|
data-auto-scale="true"
|
|
data-show-zero-line="true"
|
|
style="height: 300px;"
|
|
>
|
|
<canvas></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<!-- Right Column: Graphs and Metrics -->
|
|
<div class="space-y-6">
|
|
<!-- CPU Usage Chart -->
|
|
<%= if @cpu_chart_data do %>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<.link
|
|
navigate={
|
|
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/processors"
|
|
}
|
|
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
Processors
|
|
</h3>
|
|
<.icon name="hero-arrow-right" class="h-4 w-4 text-zinc-400" />
|
|
</div>
|
|
</.link>
|
|
<div class="p-4">
|
|
<div
|
|
id="cpu-chart"
|
|
phx-hook="SensorChart"
|
|
data-chart={@cpu_chart_data}
|
|
style="height: 300px;"
|
|
>
|
|
<canvas></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<!-- Memory Usage Chart -->
|
|
<%= if @memory_chart_data do %>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<.link
|
|
navigate={
|
|
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/memory"
|
|
}
|
|
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
Memory Usage
|
|
</h3>
|
|
<.icon name="hero-arrow-right" class="h-4 w-4 text-zinc-400" />
|
|
</div>
|
|
</.link>
|
|
<div class="p-4">
|
|
<div
|
|
id="memory-chart"
|
|
phx-hook="SensorChart"
|
|
data-chart={@memory_chart_data}
|
|
style="height: 300px;"
|
|
>
|
|
<canvas></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<!-- Storage Usage -->
|
|
<%= if @storage_sensors && length(@storage_sensors) > 0 do %>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
Storage Usage
|
|
</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<dl class="space-y-3">
|
|
<%= for sensor <- @storage_sensors do %>
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
|
|
<.link
|
|
navigate={
|
|
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/storage"
|
|
}
|
|
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
|
>
|
|
{sensor.sensor_descr}
|
|
</.link>
|
|
</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
<%= if sensor.latest_reading do %>
|
|
{format_sensor_value(
|
|
sensor.latest_reading.value,
|
|
sensor.sensor_divisor
|
|
)}
|
|
{sensor.sensor_unit}
|
|
<% else %>
|
|
N/A
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<!-- Temperature Sensors -->
|
|
<%= if @temperature_sensors && length(@temperature_sensors) > 0 do %>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
Temperature
|
|
</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<dl class="space-y-3">
|
|
<%= for sensor <- @temperature_sensors do %>
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
|
|
<.link
|
|
navigate={
|
|
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/temperature"
|
|
}
|
|
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
|
>
|
|
{sensor.sensor_descr}
|
|
</.link>
|
|
</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
<%= if sensor.latest_reading do %>
|
|
{format_sensor_value(
|
|
sensor.latest_reading.value,
|
|
sensor.sensor_divisor
|
|
)}
|
|
{sensor.sensor_unit}
|
|
<% else %>
|
|
N/A
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<!-- Voltage Sensors -->
|
|
<%= if @voltage_sensors && length(@voltage_sensors) > 0 do %>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Voltage</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<dl class="space-y-3">
|
|
<%= for sensor <- @voltage_sensors do %>
|
|
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
|
|
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
|
|
<.link
|
|
navigate={
|
|
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/voltage"
|
|
}
|
|
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
|
>
|
|
{sensor.sensor_descr}
|
|
</.link>
|
|
</dt>
|
|
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
<%= if sensor.latest_reading do %>
|
|
{format_sensor_value(
|
|
sensor.latest_reading.value,
|
|
sensor.sensor_divisor
|
|
)}
|
|
{sensor.sensor_unit}
|
|
<% else %>
|
|
N/A
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% "ports" -> %>
|
|
<%= if @snmp_interfaces && length(@snmp_interfaces) > 0 do %>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
Network Interfaces
|
|
</h3>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
|
<thead class="bg-zinc-50 dark:bg-zinc-900">
|
|
<tr class="text-xs text-zinc-600 dark:text-zinc-400">
|
|
<th class="px-4 py-3 text-left font-medium">#</th>
|
|
<th class="px-4 py-3 text-left font-medium">Name</th>
|
|
<th class="px-4 py-3 text-left font-medium">Status</th>
|
|
<th class="px-4 py-3 text-left font-medium">Speed</th>
|
|
<th class="px-4 py-3 text-left font-medium">MAC</th>
|
|
<th class="px-4 py-3 text-left font-medium">Type</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-700">
|
|
<%= for interface <- @snmp_interfaces do %>
|
|
<tr class="text-sm hover:bg-zinc-50 dark:hover:bg-zinc-900">
|
|
<td class="px-4 py-3 text-zinc-900 dark:text-zinc-100">
|
|
{interface.if_index}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<div class="font-medium text-zinc-900 dark:text-zinc-100">
|
|
{interface.if_name || interface.if_descr}
|
|
</div>
|
|
<%= if interface.if_alias do %>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
|
{interface.if_alias}
|
|
</div>
|
|
<% end %>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span class={[
|
|
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
|
|
interface.if_oper_status == "up" &&
|
|
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
interface.if_oper_status == "down" &&
|
|
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
|
interface.if_oper_status not in ["up", "down"] &&
|
|
"bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
|
|
]}>
|
|
{String.upcase(interface.if_oper_status || "unknown")}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-zinc-900 dark:text-zinc-100">
|
|
{format_speed(interface.if_speed)}
|
|
</td>
|
|
<td class="px-4 py-3 font-mono text-xs text-zinc-600 dark:text-zinc-400">
|
|
{interface.if_phys_address || "-"}
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-zinc-600 dark:text-zinc-400">
|
|
{interface.if_type || "-"}
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="text-center py-12">
|
|
<p class="text-zinc-500 dark:text-zinc-400">No interfaces discovered</p>
|
|
</div>
|
|
<% end %>
|
|
<% "logs" -> %>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Event Logs</h3>
|
|
<p class="text-xs text-zinc-600 dark:text-zinc-400 mt-1">
|
|
Showing the most recent 100 events
|
|
</p>
|
|
</div>
|
|
<%= if Enum.any?(@events) do %>
|
|
<div class="divide-y divide-zinc-200 dark:divide-zinc-700">
|
|
<%= for event <- @events do %>
|
|
<div class="px-4 py-3 hover:bg-zinc-50 dark:hover:bg-zinc-750">
|
|
<div class="flex items-start gap-3">
|
|
<!-- Severity Icon -->
|
|
<div class={[
|
|
"flex-shrink-0 mt-0.5",
|
|
event.severity == "info" && "text-blue-500",
|
|
event.severity == "warning" && "text-yellow-500",
|
|
event.severity == "critical" && "text-red-500"
|
|
]}>
|
|
<%= if event.severity == "info" do %>
|
|
<.icon name="hero-information-circle" class="w-5 h-5" />
|
|
<% end %>
|
|
<%= if event.severity == "warning" do %>
|
|
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
|
|
<% end %>
|
|
<%= if event.severity == "critical" do %>
|
|
<.icon name="hero-x-circle" class="w-5 h-5" />
|
|
<% end %>
|
|
</div>
|
|
<!-- Event Details -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{event.message}
|
|
</p>
|
|
<div class="mt-1 flex items-center gap-3 text-xs text-zinc-500 dark:text-zinc-400">
|
|
<span>{format_event_type(event.event_type)}</span>
|
|
<span>•</span>
|
|
<span class="capitalize">{event.severity}</span>
|
|
</div>
|
|
</div>
|
|
<time class="flex-shrink-0 text-xs text-zinc-500 dark:text-zinc-400">
|
|
{time_ago(event.occurred_at)}
|
|
</time>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<div class="p-8 text-center">
|
|
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-zinc-400" />
|
|
<h3 class="mt-2 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
No events yet
|
|
</h3>
|
|
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
|
Events will appear here as interface states change, speeds change, or other monitored changes occur.
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% _ -> %>
|
|
<div class="text-center py-12">
|
|
<p class="text-zinc-500 dark:text-zinc-400">Tab not found</p>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</Layouts.authenticated>
|