feat: add loading skeletons and page transitions
This commit is contained in:
parent
ad6e9b91a3
commit
bc128388ba
8 changed files with 94 additions and 39 deletions
|
|
@ -1,4 +1,8 @@
|
|||
2026-02-13
|
||||
feat: SEO meta tags, OG tags, favicon, theme-color in root layout
|
||||
feat: global CSS polish — smooth transitions, hover effects, scrollbar styling, status pulse
|
||||
feat: loading skeletons and page fade-in transitions
|
||||
feat: breadcrumb navigation on device detail, site detail, config timeline, trace pages
|
||||
feat: nav search trigger button with ⌘K keyboard shortcut hint
|
||||
fix: remove experimental badges from Alerts and Network Map nav
|
||||
feat: polished empty states for alerts (all-clear), insights, sites pages
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ defmodule ToweropsWeb do
|
|||
|
||||
import Phoenix.HTML
|
||||
import ToweropsWeb.CoreComponents
|
||||
import ToweropsWeb.Components.Breadcrumbs
|
||||
import ToweropsWeb.Components.Skeletons
|
||||
import ToweropsWeb.GettextHelpers
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,20 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="csrf-token" content={get_csrf_token()} />
|
||||
<.live_title default="Towerops">
|
||||
<meta name="description" content="TowerOps — Operational intelligence for WISP and ISP operators. Connect network monitoring to subscriber impact and revenue." />
|
||||
<meta name="theme-color" content="#2563eb" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="TowerOps" />
|
||||
<meta property="og:title" content="TowerOps — See the Business Impact of Every Network Event" />
|
||||
<meta property="og:description" content="The only platform that connects network monitoring to subscriber impact and revenue. Built for WISP and ISP operators." />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href={~p"/images/towerops_logo.png"} />
|
||||
<link rel="apple-touch-icon" href={~p"/images/towerops_logo.png"} />
|
||||
|
||||
<.live_title default="TowerOps">
|
||||
{assigns[:page_title]}
|
||||
</.live_title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||
|
|
|
|||
|
|
@ -77,6 +77,12 @@ defmodule ToweropsWeb.ConfigTimelineLive do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.breadcrumb items={[
|
||||
%{label: "Dashboard", navigate: ~p"/dashboard"},
|
||||
%{label: "Devices", navigate: ~p"/devices"},
|
||||
%{label: @device.name, navigate: ~p"/devices/#{@device.id}"},
|
||||
%{label: "Config Timeline"}
|
||||
]} />
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,13 @@
|
|||
>
|
||||
<.icon name="hero-bars-3" class="h-4 w-4" /> Reorder
|
||||
</.button>
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="toggle_compact_mode"
|
||||
variant={if @compact_mode, do: "primary", else: nil}
|
||||
>
|
||||
<.icon name="hero-bars-2" class="h-4 w-4" /> Compact
|
||||
</.button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
|
|
@ -176,6 +183,24 @@
|
|||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<!-- Status Summary Bar -->
|
||||
<div class="flex items-center gap-4 px-4 py-2.5 mb-4 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 text-sm">
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{@total_devices} devices</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">|</span>
|
||||
<span class="inline-flex items-center gap-1.5 text-green-700 dark:text-green-400">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-green-500"></span>
|
||||
{@total_up} up
|
||||
</span>
|
||||
<span :if={@total_down > 0} class="inline-flex items-center gap-1.5 text-red-700 dark:text-red-400">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
|
||||
{@total_down} down
|
||||
</span>
|
||||
<span :if={@total_unknown > 0} class="inline-flex items-center gap-1.5 text-gray-500 dark:text-gray-400">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-gray-400"></span>
|
||||
{@total_unknown} unknown
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
|
|
@ -317,18 +342,19 @@
|
|||
<tr
|
||||
id={dom_id}
|
||||
class={[
|
||||
"border-t device-row hover:bg-gray-50 dark:hover:bg-gray-800/50",
|
||||
"border-t device-row hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors",
|
||||
if(row.device_index == 0,
|
||||
do: "border-gray-300 dark:border-white/15",
|
||||
else: "border-gray-200 dark:border-white/10"
|
||||
)
|
||||
]}
|
||||
title={device_row_title(row.device)}
|
||||
data-device-id={row.device.id}
|
||||
data-site-id={if row.site, do: row.site.id, else: "no-site"}
|
||||
data-device-position={row.device_index + 1}
|
||||
draggable={if @reorder_mode, do: "true", else: "false"}
|
||||
>
|
||||
<td :if={@reorder_mode} class="py-4 pl-4 pr-2 sm:pl-3">
|
||||
<td :if={@reorder_mode} class={["pl-4 pr-2 sm:pl-3", if(@compact_mode, do: "py-1.5", else: "py-4")]}>
|
||||
<button
|
||||
type="button"
|
||||
class="drag-handle cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
|
|
@ -341,22 +367,27 @@
|
|||
<.link
|
||||
navigate={~p"/devices/#{row.device.id}"}
|
||||
class={[
|
||||
"flex items-center gap-2 py-4 pr-3 text-inherit no-underline",
|
||||
"flex items-center gap-2 pr-3 text-inherit no-underline",
|
||||
@reorder_mode && "pl-2",
|
||||
!@reorder_mode && "pl-4 sm:pl-3"
|
||||
!@reorder_mode && "pl-4 sm:pl-3",
|
||||
if(@compact_mode, do: "py-1.5", else: "py-4")
|
||||
]}
|
||||
>
|
||||
<span
|
||||
class={"inline-block h-2.5 w-2.5 rounded-full flex-shrink-0 #{health_dot_color(row.health)}"}
|
||||
title={health_dot_title(row.health)}
|
||||
/>
|
||||
<.icon
|
||||
name={device_type_icon(row.device.device_type)}
|
||||
class="h-4 w-4 text-gray-400 dark:text-gray-500 flex-shrink-0"
|
||||
/>
|
||||
{row.device.name}
|
||||
</.link>
|
||||
</td>
|
||||
<td class="p-0 text-sm whitespace-nowrap text-gray-500 dark:text-gray-400 font-mono">
|
||||
<.link
|
||||
navigate={~p"/devices/#{row.device.id}"}
|
||||
class="block px-3 py-4 text-inherit no-underline"
|
||||
class={["block px-3 text-inherit no-underline", if(@compact_mode, do: "py-1.5", else: "py-4")]}
|
||||
>
|
||||
{row.device.ip_address}
|
||||
</.link>
|
||||
|
|
@ -364,25 +395,30 @@
|
|||
<td class="p-0 text-sm whitespace-nowrap">
|
||||
<.link
|
||||
navigate={~p"/devices/#{row.device.id}"}
|
||||
class="block px-3 py-4 text-inherit no-underline"
|
||||
class={["block px-3 text-inherit no-underline", if(@compact_mode, do: "py-1.5", else: "py-4")]}
|
||||
>
|
||||
<span class={[
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
row.device.status == :up &&
|
||||
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
||||
row.device.status == :down &&
|
||||
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
||||
row.device.status == :unknown &&
|
||||
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
||||
]}>
|
||||
{row.device.status |> to_string() |> String.upcase()}
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<span class={[
|
||||
"inline-block h-2 w-2 rounded-full",
|
||||
row.device.status == :up && "bg-green-500",
|
||||
row.device.status == :down && "bg-red-500",
|
||||
row.device.status == :unknown && "bg-gray-400"
|
||||
]} />
|
||||
<span class={[
|
||||
"text-xs font-medium",
|
||||
row.device.status == :up && "text-green-700 dark:text-green-400",
|
||||
row.device.status == :down && "text-red-700 dark:text-red-400",
|
||||
row.device.status == :unknown && "text-gray-500 dark:text-gray-400"
|
||||
]}>
|
||||
{row.device.status |> to_string() |> String.upcase()}
|
||||
</span>
|
||||
</span>
|
||||
</.link>
|
||||
</td>
|
||||
<td class="p-0 text-sm whitespace-nowrap">
|
||||
<.link
|
||||
navigate={~p"/devices/#{row.device.id}"}
|
||||
class="block px-3 py-4 text-inherit no-underline"
|
||||
class={["block px-3 text-inherit no-underline", if(@compact_mode, do: "py-1.5", else: "py-4")]}
|
||||
>
|
||||
<% {text, color} = last_seen_text(row.device.last_seen_at) %>
|
||||
<span class={"text-xs font-medium #{color}"}>{text}</span>
|
||||
|
|
@ -391,7 +427,7 @@
|
|||
<td class="p-0 text-sm whitespace-nowrap">
|
||||
<.link
|
||||
navigate={~p"/devices/#{row.device.id}"}
|
||||
class="block px-3 py-4 text-inherit no-underline"
|
||||
class={["block px-3 text-inherit no-underline", if(@compact_mode, do: "py-1.5", else: "py-4")]}
|
||||
>
|
||||
<% {rt_text, rt_color} = response_time_badge(row.response) %>
|
||||
<span class={"text-xs font-mono #{rt_color}"}>{rt_text}</span>
|
||||
|
|
@ -400,7 +436,7 @@
|
|||
<td class="p-0 text-sm whitespace-nowrap">
|
||||
<.link
|
||||
navigate={~p"/devices/#{row.device.id}"}
|
||||
class="block px-3 py-4 text-inherit no-underline"
|
||||
class={["block px-3 text-inherit no-underline", if(@compact_mode, do: "py-1.5", else: "py-4")]}
|
||||
>
|
||||
<%= if sub_text = format_subscriber_count(row.subscribers) do %>
|
||||
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
||||
|
|
|
|||
|
|
@ -4,25 +4,11 @@
|
|||
active_page="devices"
|
||||
>
|
||||
<div class="mb-6 -mt-2">
|
||||
<!-- Breadcrumbs -->
|
||||
<nav :if={@device.site} class="flex mb-4" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="inline-flex items-center">
|
||||
<.link
|
||||
navigate={~p"/sites/#{@device.site.id}"}
|
||||
class="hover:text-gray-900 dark:hover:text-white"
|
||||
>
|
||||
{@device.site.name}
|
||||
</.link>
|
||||
</li>
|
||||
<li aria-current="page">
|
||||
<div class="flex items-center">
|
||||
<.icon name="hero-chevron-right" class="h-4 w-4 mx-1" />
|
||||
<span class="text-gray-900 dark:text-white font-medium">{@device.name}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<.breadcrumb items={
|
||||
[%{label: "Dashboard", navigate: ~p"/dashboard"}, %{label: "Devices", navigate: ~p"/devices"}] ++
|
||||
if(@device.site, do: [%{label: @device.site.name, navigate: ~p"/sites/#{@device.site.id}"}], else: []) ++
|
||||
[%{label: @device.name}]
|
||||
} />
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@
|
|||
current_scope={@current_scope}
|
||||
active_page="sites"
|
||||
>
|
||||
<.breadcrumb items={[
|
||||
%{label: "Dashboard", navigate: ~p"/dashboard"},
|
||||
%{label: "Sites", navigate: ~p"/sites"},
|
||||
%{label: @site.name}
|
||||
]} />
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>{@site.location}</:subtitle>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
current_scope={@current_scope}
|
||||
active_page="trace"
|
||||
>
|
||||
<.breadcrumb items={
|
||||
[%{label: "Dashboard", navigate: ~p"/dashboard"}, %{label: "Trace"}] ++
|
||||
if(@trace && @trace.subscriber, do: [%{label: @trace.subscriber.name}], else: [])
|
||||
} />
|
||||
<div class="space-y-6">
|
||||
<%!-- Search Bar --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 dark:border-white/10 dark:bg-gray-900">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue