ui: polish device detail, site detail, and user settings
This commit is contained in:
parent
5041ac8e25
commit
e5796b3464
3 changed files with 325 additions and 221 deletions
|
|
@ -180,9 +180,12 @@ defmodule ToweropsWeb.SiteLive.Show do
|
|||
defp change_size_label(size) when size < 500, do: "Medium"
|
||||
defp change_size_label(_), do: "Large"
|
||||
|
||||
defp change_size_color(size) when size < 100, do: "badge-ghost"
|
||||
defp change_size_color(size) when size < 500, do: "badge-warning"
|
||||
defp change_size_color(_), do: "badge-error"
|
||||
defp change_size_color(size) when size < 100, do: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
||||
|
||||
defp change_size_color(size) when size < 500,
|
||||
do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
|
||||
defp change_size_color(_), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
|
||||
|
||||
defp insight_urgency_class("critical"), do: "text-red-600 dark:text-red-400"
|
||||
defp insight_urgency_class("warning"), do: "text-yellow-600 dark:text-yellow-400"
|
||||
|
|
|
|||
|
|
@ -35,77 +35,93 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>{@site.location}</:subtitle>
|
||||
<:actions>
|
||||
<%= if @device != [] do %>
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="force_rediscover_all"
|
||||
data-confirm={
|
||||
t("This will trigger SNMP discovery for all devices at this site. Continue?")
|
||||
}
|
||||
>
|
||||
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Force Rediscover All
|
||||
</.button>
|
||||
<% end %>
|
||||
<.button navigate={~p"/sites/#{@site.id}/edit"}>
|
||||
<.icon name="hero-pencil" class="h-4 w-4" /> Edit
|
||||
<%= if @device != [] do %>
|
||||
<div class="mt-2">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="force_rediscover_all"
|
||||
class="btn-sm"
|
||||
data-confirm={
|
||||
t("This will trigger SNMP discovery for all devices at this site. Continue?")
|
||||
}
|
||||
>
|
||||
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> {t("Force Rediscover All")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%!-- Quick Actions --%>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<.button navigate={~p"/devices/new?site_id=#{@site.id}"} variant="primary" class="btn-sm">
|
||||
<.icon name="hero-plus" class="h-4 w-4" /> {t("Add Device")}
|
||||
</.button>
|
||||
<%= if @site.latitude && @site.longitude do %>
|
||||
<.button
|
||||
navigate={~p"/sites-map?lat=#{@site.latitude}&lng=#{@site.longitude}&zoom=15"}
|
||||
class="btn-sm"
|
||||
>
|
||||
<.icon name="hero-map" class="h-4 w-4" /> {t("View on Map")}
|
||||
</.button>
|
||||
<% end %>
|
||||
<.button navigate={~p"/sites/#{@site.id}/edit"} class="btn-sm">
|
||||
<.icon name="hero-pencil" class="h-4 w-4" /> {t("Edit Site")}
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<%!-- Summary Stat Cards --%>
|
||||
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<%!-- Subscribers --%>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<.icon name="hero-users" class="h-4 w-4" /> Subscribers
|
||||
</div>
|
||||
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{format_number(@site_summary.subscribers || 0)}
|
||||
</p>
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<.icon name="hero-users" class="h-4 w-4" /> {t("Subscribers")}
|
||||
</div>
|
||||
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white font-mono">
|
||||
{format_number(@site_summary.subscribers || 0)}
|
||||
</p>
|
||||
</div>
|
||||
<%!-- MRR --%>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<.icon name="hero-currency-dollar" class="h-4 w-4" /> Monthly Revenue
|
||||
</div>
|
||||
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{format_mrr(@site_summary.mrr)}
|
||||
</p>
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<.icon name="hero-currency-dollar" class="h-4 w-4" /> {t("Monthly Revenue")}
|
||||
</div>
|
||||
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{format_mrr(@site_summary.mrr)}
|
||||
</p>
|
||||
</div>
|
||||
<%!-- Device Count --%>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<.icon name="hero-server" class="h-4 w-4" /> Devices
|
||||
</div>
|
||||
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{format_number(@site_summary.device_count)}
|
||||
<%= if @site_summary.devices_down > 0 do %>
|
||||
<span class="text-sm font-medium text-red-600 dark:text-red-400">
|
||||
({@site_summary.devices_down} down)
|
||||
</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<.icon name="hero-server" class="h-4 w-4" /> {t("Devices")}
|
||||
</div>
|
||||
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white font-mono">
|
||||
{format_number(@site_summary.device_count)}
|
||||
<%= if @site_summary.devices_down > 0 do %>
|
||||
<span class="text-sm font-medium text-red-600 dark:text-red-400">
|
||||
({@site_summary.devices_down} {t("down")})
|
||||
</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<%!-- Active Alerts --%>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<.icon name="hero-bell-alert" class="h-4 w-4" /> Active Alerts
|
||||
</div>
|
||||
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{length(@active_alerts)}
|
||||
</p>
|
||||
<div class={[
|
||||
"rounded-lg border bg-white dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm",
|
||||
if(length(@active_alerts) > 0,
|
||||
do: "border-red-200 dark:border-red-900/50",
|
||||
else: "border-gray-200 dark:border-white/10"
|
||||
)
|
||||
]}>
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<.icon name="hero-bell-alert" class="h-4 w-4" /> {t("Active Alerts")}
|
||||
</div>
|
||||
<p class={[
|
||||
"mt-1 text-2xl font-bold font-mono",
|
||||
if(length(@active_alerts) > 0,
|
||||
do: "text-red-600 dark:text-red-400",
|
||||
else: "text-gray-900 dark:text-white"
|
||||
)
|
||||
]}>
|
||||
{length(@active_alerts)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -145,9 +161,11 @@
|
|||
<%!-- Left Column --%>
|
||||
<div class="space-y-6">
|
||||
<%!-- Site Details --%>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="card-title text-base">{t("Site Details")}</h3>
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
||||
{t("Site Details")}
|
||||
</h3>
|
||||
<dl class="mt-2 space-y-3">
|
||||
<%= if @site.parent_site do %>
|
||||
<div>
|
||||
|
|
@ -193,12 +211,14 @@
|
|||
</div>
|
||||
|
||||
<%!-- Active Alerts Panel --%>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="card-title text-base">
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
||||
<.icon name="hero-bell-alert" class="h-5 w-5" /> Active Alerts
|
||||
<%= if @active_alerts != [] do %>
|
||||
<span class="badge badge-error badge-sm">{length(@active_alerts)}</span>
|
||||
<span class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400">
|
||||
{length(@active_alerts)}
|
||||
</span>
|
||||
<% end %>
|
||||
</h3>
|
||||
<%= if @active_alerts == [] do %>
|
||||
|
|
@ -210,7 +230,7 @@
|
|||
<div class="mt-2 space-y-2">
|
||||
<div
|
||||
:for={alert <- @active_alerts}
|
||||
class="flex items-start gap-3 rounded-lg border border-base-300 p-3"
|
||||
class="flex items-start gap-3 rounded-lg border border-gray-200 dark:border-white/10 p-3"
|
||||
>
|
||||
<.icon
|
||||
name={insight_urgency_icon(alert_severity(alert.alert_type))}
|
||||
|
|
@ -232,9 +252,9 @@
|
|||
</div>
|
||||
|
||||
<%!-- Recent Config Changes Panel --%>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="card-title text-base">
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
||||
<.icon name="hero-document-text" class="h-5 w-5" /> Recent Config Changes
|
||||
</h3>
|
||||
<%= if @config_changes == [] do %>
|
||||
|
|
@ -245,7 +265,7 @@
|
|||
<div class="mt-2 space-y-2">
|
||||
<div
|
||||
:for={change <- @config_changes}
|
||||
class="flex items-center justify-between rounded-lg border border-base-300 p-3"
|
||||
class="flex items-center justify-between rounded-lg border border-gray-200 dark:border-white/10 p-3"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
|
|
@ -268,10 +288,10 @@
|
|||
<%!-- Right Column --%>
|
||||
<div class="space-y-6">
|
||||
<%!-- Device Health Grid --%>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="card-title text-base">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
||||
<.icon name="hero-server" class="h-5 w-5" /> Device Health
|
||||
</h3>
|
||||
<.button
|
||||
|
|
@ -283,7 +303,7 @@
|
|||
</.button>
|
||||
</div>
|
||||
<%= if @device == [] do %>
|
||||
<div class="mt-2 rounded-lg border-2 border-dashed border-base-300 p-8 text-center">
|
||||
<div class="mt-2 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 p-8 text-center">
|
||||
<.icon
|
||||
name="hero-server"
|
||||
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
|
||||
|
|
@ -301,23 +321,49 @@
|
|||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="mt-2 grid gap-2 sm:grid-cols-2">
|
||||
<.link
|
||||
:for={eq <- @device}
|
||||
navigate={~p"/devices/#{eq.id}"}
|
||||
class="flex items-center gap-3 rounded-lg border border-base-300 p-3 hover:bg-base-200 transition-colors cursor-pointer"
|
||||
>
|
||||
<span class={["h-3 w-3 shrink-0 rounded-full", status_dot_class(eq.status)]} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{eq.name}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{eq.ip_address}</p>
|
||||
</div>
|
||||
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
|
||||
{format_response_time(@response_times[eq.id])}
|
||||
</span>
|
||||
</.link>
|
||||
<div class="mt-2 overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
|
||||
<thead>
|
||||
<tr class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<th class="px-3 py-2 text-left font-medium">{t("Device")}</th>
|
||||
<th class="px-3 py-2 text-left font-medium">{t("IP Address")}</th>
|
||||
<th class="px-3 py-2 text-right font-medium">{t("Latency")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-white/5">
|
||||
<tr
|
||||
:for={eq <- @device}
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
|
||||
phx-click={JS.navigate(~p"/devices/#{eq.id}")}
|
||||
>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class={[
|
||||
"h-2 w-2 shrink-0 rounded-full",
|
||||
status_dot_class(eq.status)
|
||||
]} />
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{eq.name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 font-mono text-xs text-gray-500 dark:text-gray-400">
|
||||
{eq.ip_address}
|
||||
</td>
|
||||
<td class={[
|
||||
"px-3 py-2 text-right font-mono text-xs",
|
||||
case @response_times[eq.id] do
|
||||
nil -> "text-gray-400"
|
||||
ms when ms < 50 -> "text-green-600 dark:text-green-400"
|
||||
ms when ms < 200 -> "text-yellow-600 dark:text-yellow-400"
|
||||
_ -> "text-red-600 dark:text-red-400"
|
||||
end
|
||||
]}>
|
||||
{format_response_time(@response_times[eq.id])}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -325,15 +371,15 @@
|
|||
|
||||
<%!-- Insights Panel --%>
|
||||
<%= if @insights != [] do %>
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="card-title text-base">
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
||||
<.icon name="hero-light-bulb" class="h-5 w-5" /> Preseem Insights
|
||||
</h3>
|
||||
<div class="mt-2 space-y-2">
|
||||
<div
|
||||
:for={insight <- @insights}
|
||||
class="flex items-start gap-3 rounded-lg border border-base-300 p-3"
|
||||
class="flex items-start gap-3 rounded-lg border border-gray-200 dark:border-white/10 p-3"
|
||||
>
|
||||
<.icon
|
||||
name={insight_urgency_icon(insight.urgency)}
|
||||
|
|
@ -358,22 +404,30 @@
|
|||
<%!-- Latency Chart --%>
|
||||
<%= if @latency_chart_data do %>
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="card-title text-base">
|
||||
<.icon name="hero-chart-bar" class="h-5 w-5" /> Site Latency — Last 24 Hours
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
||||
<.icon name="hero-chart-bar" class="h-4 w-4" /> {t("Site Latency — Last 24 Hours")}
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
id="site-latency-chart"
|
||||
phx-hook="SensorChart"
|
||||
data-chart={@latency_chart_data}
|
||||
data-unit="ms"
|
||||
data-auto-scale="true"
|
||||
style="height: 400px;"
|
||||
>
|
||||
<canvas></canvas>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div
|
||||
id="site-latency-chart"
|
||||
phx-hook="SensorChart"
|
||||
data-chart={@latency_chart_data}
|
||||
data-unit="ms"
|
||||
data-auto-scale="true"
|
||||
class="relative"
|
||||
style="height: 400px;"
|
||||
>
|
||||
<div class="absolute inset-0 flex items-center justify-center" data-loading>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-2 border-gray-200 border-t-blue-500">
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">{t("Loading chart...")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<canvas></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,95 +11,108 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<header class="border-b border-gray-200 dark:border-white/5">
|
||||
<nav class="flex overflow-x-auto py-4">
|
||||
<ul
|
||||
role="list"
|
||||
class="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-gray-500 sm:px-6 lg:px-8 dark:text-gray-400"
|
||||
<div class="border-b border-gray-200 dark:border-white/10">
|
||||
<nav class="-mb-px flex space-x-8 overflow-x-auto">
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=personal"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
|
||||
if(@active_tab == "personal",
|
||||
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
|
||||
else:
|
||||
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<li>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=personal"}
|
||||
class={
|
||||
if @active_tab == "personal",
|
||||
do: "text-indigo-600 dark:text-indigo-400",
|
||||
else: ""
|
||||
}
|
||||
>
|
||||
{t("Personal")}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=account"}
|
||||
class={
|
||||
if @active_tab == "account",
|
||||
do: "text-indigo-600 dark:text-indigo-400",
|
||||
else: ""
|
||||
}
|
||||
>
|
||||
{t("Account")}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=sessions"}
|
||||
class={
|
||||
if @active_tab == "sessions",
|
||||
do: "text-indigo-600 dark:text-indigo-400",
|
||||
else: ""
|
||||
}
|
||||
>
|
||||
{t("Sessions")}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=api"}
|
||||
class={
|
||||
if @active_tab == "api",
|
||||
do: "text-indigo-600 dark:text-indigo-400",
|
||||
else: ""
|
||||
}
|
||||
>
|
||||
{t("API")}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=notifications"}
|
||||
class={
|
||||
if @active_tab == "notifications",
|
||||
do: "text-indigo-600 dark:text-indigo-400",
|
||||
else: ""
|
||||
}
|
||||
>
|
||||
{t("Notifications")}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=security"}
|
||||
class={
|
||||
if @active_tab == "security",
|
||||
do: "text-indigo-600 dark:text-indigo-400",
|
||||
else: ""
|
||||
}
|
||||
>
|
||||
{t("Security")}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
navigate={~p"/users/my-data"}
|
||||
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
|
||||
>
|
||||
{t("My Data")}
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<.icon name="hero-user" class="h-4 w-4" /> {t("Personal")}
|
||||
</span>
|
||||
</.link>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=account"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
|
||||
if(@active_tab == "account",
|
||||
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
|
||||
else:
|
||||
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<.icon name="hero-envelope" class="h-4 w-4" /> {t("Account")}
|
||||
</span>
|
||||
</.link>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=sessions"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
|
||||
if(@active_tab == "sessions",
|
||||
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
|
||||
else:
|
||||
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<.icon name="hero-computer-desktop" class="h-4 w-4" /> {t("Sessions")}
|
||||
</span>
|
||||
</.link>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=api"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
|
||||
if(@active_tab == "api",
|
||||
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
|
||||
else:
|
||||
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<.icon name="hero-code-bracket" class="h-4 w-4" /> {t("API")}
|
||||
</span>
|
||||
</.link>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=notifications"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
|
||||
if(@active_tab == "notifications",
|
||||
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
|
||||
else:
|
||||
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<.icon name="hero-bell" class="h-4 w-4" /> {t("Notifications")}
|
||||
</span>
|
||||
</.link>
|
||||
<.link
|
||||
patch={~p"/users/settings?tab=security"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
|
||||
if(@active_tab == "security",
|
||||
do: "border-indigo-500 text-indigo-600 dark:text-indigo-400 font-semibold",
|
||||
else:
|
||||
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<.icon name="hero-shield-check" class="h-4 w-4" /> {t("Security")}
|
||||
</span>
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/users/my-data"}
|
||||
class="whitespace-nowrap py-4 px-1 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<.icon name="hero-arrow-down-tray" class="h-4 w-4" /> {t("My Data")}
|
||||
</span>
|
||||
</.link>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-gray-200 dark:divide-white/10">
|
||||
<!-- Personal Information Section -->
|
||||
|
|
@ -777,27 +790,38 @@
|
|||
<%= for session <- @browser_sessions do %>
|
||||
<% is_current = current_session?(session, get_current_token_id(assigns)) %>
|
||||
<li class="flex items-center justify-between gap-x-6 py-5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-x-3">
|
||||
<p class="text-sm/6 font-semibold text-gray-900 dark:text-white">
|
||||
{format_browser_info(session)}
|
||||
</p>
|
||||
<%= if is_current do %>
|
||||
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 inset-ring-1 inset-ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:inset-ring-green-500/20">
|
||||
{t("Current Session")}
|
||||
</span>
|
||||
<div class="flex items-start gap-x-3 min-w-0 flex-1">
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<%= if String.contains?(String.downcase(session.user_agent || ""), ["mobile", "android", "iphone", "ipad"]) do %>
|
||||
<.icon name="hero-device-phone-mobile" class="h-6 w-6 text-gray-400" />
|
||||
<% else %>
|
||||
<.icon name="hero-computer-desktop" class="h-6 w-6 text-gray-400" />
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-col gap-y-1 text-xs/5 text-gray-500 dark:text-gray-400">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<.icon name="hero-map-pin" class="h-3 w-3" />
|
||||
<span>{format_location(session)}</span>
|
||||
<span class="text-gray-400 dark:text-gray-600">•</span>
|
||||
<span class="font-mono">{session.ip_address}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-x-3">
|
||||
<p class="text-sm/6 font-semibold text-gray-900 dark:text-white">
|
||||
{format_browser_info(session)}
|
||||
</p>
|
||||
<%= if is_current do %>
|
||||
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 inset-ring-1 inset-ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:inset-ring-green-500/20">
|
||||
{t("Current Session")}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<.icon name="hero-clock" class="h-3 w-3" />
|
||||
<span>Last active {format_relative_time(session.last_activity_at)}</span>
|
||||
<div class="mt-1 flex flex-col gap-y-1 text-xs/5 text-gray-500 dark:text-gray-400">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<.icon name="hero-map-pin" class="h-3 w-3" />
|
||||
<span>{format_location(session)}</span>
|
||||
<span class="text-gray-400 dark:text-gray-600">•</span>
|
||||
<span class="font-mono">{session.ip_address}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<.icon name="hero-clock" class="h-3 w-3" />
|
||||
<span>
|
||||
Last active {format_relative_time(session.last_activity_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1572,12 +1596,35 @@
|
|||
>
|
||||
{t("Scan QR Code")}
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t(
|
||||
"Scan this QR code with your authenticator app (Google Authenticator, Authy, 1Password, etc.)."
|
||||
)}
|
||||
</p>
|
||||
<div class="mt-4 text-left">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 text-xs font-bold text-indigo-600 dark:bg-indigo-900 dark:text-indigo-400">
|
||||
1
|
||||
</span>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{t(
|
||||
"Install an authenticator app (Google Authenticator, Authy, 1Password, etc.)"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 text-xs font-bold text-indigo-600 dark:bg-indigo-900 dark:text-indigo-400">
|
||||
2
|
||||
</span>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{t("Scan the QR code below with your authenticator app")}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="flex-shrink-0 flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 text-xs font-bold text-indigo-600 dark:bg-indigo-900 dark:text-indigo-400">
|
||||
3
|
||||
</span>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{t("Enter the 6-digit code from your app to verify setup")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code -->
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue