From 7d4f7dd7d4cda39f937172d97cb165919c0066c4 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 3 Jan 2026 12:28:26 -0600 Subject: [PATCH] Add active page navigation highlighting - Add active_page attribute to authenticated layout - Update nav_link component to show active state with border and text styling - Update all LiveView templates to pass active_page parameter - Fix datetime truncation issues in tests and schemas for :utc_datetime fields - Fix dashboard test assertions for equipment status text --- lib/towerops/organizations.ex | 5 ++- lib/towerops/organizations/invitation.ex | 2 +- lib/towerops_web/components/layouts.ex | 36 +++++++++++++++---- .../live/alert_live/index.html.heex | 6 +++- .../live/dashboard_live.html.heex | 6 +++- .../live/equipment_live/index.html.heex | 6 +++- .../live/equipment_live/show.html.heex | 6 +++- .../live/site_live/index.html.heex | 6 +++- .../live/site_live/show.html.heex | 6 +++- test/towerops/organizations_test.exs | 20 +++++------ .../towerops_web/live/dashboard_live_test.exs | 4 +-- 11 files changed, 77 insertions(+), 26 deletions(-) diff --git a/lib/towerops/organizations.ex b/lib/towerops/organizations.ex index 412d8f99..21edbadb 100644 --- a/lib/towerops/organizations.ex +++ b/lib/towerops/organizations.ex @@ -204,7 +204,10 @@ defmodule Towerops.Organizations do def accept_invitation(invitation, user_id) do Ecto.Multi.new() |> Ecto.Multi.update(:invitation, fn _ -> - Ecto.Changeset.change(invitation, %{accepted_at: DateTime.utc_now(), accepted_by_id: user_id}) + Ecto.Changeset.change(invitation, %{ + accepted_at: DateTime.truncate(DateTime.utc_now(), :second), + accepted_by_id: user_id + }) end) |> Ecto.Multi.insert(:membership, fn _ -> Membership.changeset(%Membership{}, %{ diff --git a/lib/towerops/organizations/invitation.ex b/lib/towerops/organizations/invitation.ex index 45a0a70c..45413b29 100644 --- a/lib/towerops/organizations/invitation.ex +++ b/lib/towerops/organizations/invitation.ex @@ -42,7 +42,7 @@ defmodule Towerops.Organizations.Invitation do end defp set_expires_at(changeset) do - expires_at = DateTime.add(DateTime.utc_now(), 7, :day) + expires_at = DateTime.utc_now() |> DateTime.add(7, :day) |> DateTime.truncate(:second) put_change(changeset, :expires_at, expires_at) end end diff --git a/lib/towerops_web/components/layouts.ex b/lib/towerops_web/components/layouts.ex index 7ffe93cd..95f31c15 100644 --- a/lib/towerops_web/components/layouts.ex +++ b/lib/towerops_web/components/layouts.ex @@ -52,7 +52,7 @@ defmodule ToweropsWeb.Layouts do ## Examples - +

Content

@@ -63,6 +63,10 @@ defmodule ToweropsWeb.Layouts do default: nil, doc: "the current organization" + attr :active_page, :string, + default: nil, + doc: "the currently active page (dashboard, sites, equipment, or alerts)" + slot :inner_block, required: true def authenticated(assigns) do @@ -78,16 +82,28 @@ defmodule ToweropsWeb.Layouts do
- <.nav_link navigate={~p"/orgs/#{@current_organization.slug}"}> + <.nav_link + navigate={~p"/orgs/#{@current_organization.slug}"} + active={@active_page == "dashboard"} + > Dashboard - <.nav_link navigate={~p"/orgs/#{@current_organization.slug}/sites"}> + <.nav_link + navigate={~p"/orgs/#{@current_organization.slug}/sites"} + active={@active_page == "sites"} + > Sites - <.nav_link navigate={~p"/orgs/#{@current_organization.slug}/equipment"}> + <.nav_link + navigate={~p"/orgs/#{@current_organization.slug}/equipment"} + active={@active_page == "equipment"} + > Equipment - <.nav_link navigate={~p"/orgs/#{@current_organization.slug}/alerts"}> + <.nav_link + navigate={~p"/orgs/#{@current_organization.slug}/alerts"} + active={@active_page == "alerts"} + > Alerts
@@ -149,13 +165,21 @@ defmodule ToweropsWeb.Layouts do end attr :navigate, :string, required: true + attr :active, :boolean, default: false slot :inner_block, required: true defp nav_link(assigns) do ~H""" <.link navigate={@navigate} - class="inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-zinc-700 hover:border-zinc-300 hover:text-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-700 dark:hover:text-zinc-100" + class={[ + "inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium", + if(@active, + do: "border-zinc-900 text-zinc-900 dark:border-zinc-100 dark:text-zinc-100", + else: + "border-transparent text-zinc-700 hover:border-zinc-300 hover:text-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-700 dark:hover:text-zinc-100" + ) + ]} > {render_slot(@inner_block)} diff --git a/lib/towerops_web/live/alert_live/index.html.heex b/lib/towerops_web/live/alert_live/index.html.heex index 0169d27a..81febf60 100644 --- a/lib/towerops_web/live/alert_live/index.html.heex +++ b/lib/towerops_web/live/alert_live/index.html.heex @@ -1,4 +1,8 @@ - + <.header> Alerts <:subtitle>Monitor and acknowledge system alerts diff --git a/lib/towerops_web/live/dashboard_live.html.heex b/lib/towerops_web/live/dashboard_live.html.heex index 8c5b0e7b..76ec6e0a 100644 --- a/lib/towerops_web/live/dashboard_live.html.heex +++ b/lib/towerops_web/live/dashboard_live.html.heex @@ -1,4 +1,8 @@ - + <.header> Dashboard <:subtitle>Welcome to {@current_organization.name} diff --git a/lib/towerops_web/live/equipment_live/index.html.heex b/lib/towerops_web/live/equipment_live/index.html.heex index 13d77956..62ed9e46 100644 --- a/lib/towerops_web/live/equipment_live/index.html.heex +++ b/lib/towerops_web/live/equipment_live/index.html.heex @@ -1,4 +1,8 @@ - + <.header> {@page_title} <:subtitle>Monitor and manage all your network equipment diff --git a/lib/towerops_web/live/equipment_live/show.html.heex b/lib/towerops_web/live/equipment_live/show.html.heex index dc626a75..e8f22f32 100644 --- a/lib/towerops_web/live/equipment_live/show.html.heex +++ b/lib/towerops_web/live/equipment_live/show.html.heex @@ -1,4 +1,8 @@ - + <.header> {@page_title} <:subtitle>{@equipment.ip_address} diff --git a/lib/towerops_web/live/site_live/index.html.heex b/lib/towerops_web/live/site_live/index.html.heex index d7614839..f4b58993 100644 --- a/lib/towerops_web/live/site_live/index.html.heex +++ b/lib/towerops_web/live/site_live/index.html.heex @@ -1,4 +1,8 @@ - + <.header> {@page_title} <:subtitle>Manage your site locations and hierarchy diff --git a/lib/towerops_web/live/site_live/show.html.heex b/lib/towerops_web/live/site_live/show.html.heex index 3f6001dc..27eff872 100644 --- a/lib/towerops_web/live/site_live/show.html.heex +++ b/lib/towerops_web/live/site_live/show.html.heex @@ -1,4 +1,8 @@ - + <.header> {@page_title} <:subtitle>{@site.location} diff --git a/test/towerops/organizations_test.exs b/test/towerops/organizations_test.exs index dfaaa83a..40df1656 100644 --- a/test/towerops/organizations_test.exs +++ b/test/towerops/organizations_test.exs @@ -225,7 +225,7 @@ defmodule Towerops.OrganizationsTest do role: :member, invited_by_id: owner.id, token: "test-token-#{System.unique_integer()}", - expires_at: DateTime.add(DateTime.utc_now(), 7, :day) + expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second) }) invitations = Organizations.list_pending_invitations(organization.id) @@ -244,8 +244,8 @@ defmodule Towerops.OrganizationsTest do role: :member, invited_by_id: owner.id, token: "test-token-#{System.unique_integer()}", - expires_at: DateTime.add(DateTime.utc_now(), 7, :day), - accepted_at: DateTime.utc_now() + expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second), + accepted_at: DateTime.truncate(DateTime.utc_now(), :second) }) invitations = Organizations.list_pending_invitations(organization.id) @@ -263,7 +263,7 @@ defmodule Towerops.OrganizationsTest do role: :member, invited_by_id: owner.id, token: "test-token-#{System.unique_integer()}", - expires_at: DateTime.add(DateTime.utc_now(), -1, :day) + expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), -1, :day), :second) }) invitations = Organizations.list_pending_invitations(organization.id) @@ -280,7 +280,7 @@ defmodule Towerops.OrganizationsTest do role: :member, invited_by_id: owner.id, token: "unique-token-#{System.unique_integer()}", - expires_at: DateTime.add(DateTime.utc_now(), 7, :day) + expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second) } assert {:ok, invitation} = Organizations.create_invitation(valid_attrs) @@ -301,7 +301,7 @@ defmodule Towerops.OrganizationsTest do role: :member, invited_by_id: owner.id, token: token, - expires_at: DateTime.add(DateTime.utc_now(), 7, :day) + expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second) }) invitation = Organizations.get_invitation_by_token(token) @@ -321,8 +321,8 @@ defmodule Towerops.OrganizationsTest do role: :member, invited_by_id: owner.id, token: token, - expires_at: DateTime.add(DateTime.utc_now(), 7, :day), - accepted_at: DateTime.utc_now() + expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second), + accepted_at: DateTime.truncate(DateTime.utc_now(), :second) }) assert Organizations.get_invitation_by_token(token) == nil @@ -341,7 +341,7 @@ defmodule Towerops.OrganizationsTest do role: :member, invited_by_id: owner.id, token: token, - expires_at: DateTime.add(DateTime.utc_now(), 7, :day) + expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second) }) new_user = user_fixture() @@ -364,7 +364,7 @@ defmodule Towerops.OrganizationsTest do role: :member, invited_by_id: owner.id, token: "test-token-#{System.unique_integer()}", - expires_at: DateTime.add(DateTime.utc_now(), 7, :day) + expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second) }) assert {:ok, _} = Organizations.delete_invitation(invitation) diff --git a/test/towerops_web/live/dashboard_live_test.exs b/test/towerops_web/live/dashboard_live_test.exs index 057d4988..dcc49050 100644 --- a/test/towerops_web/live/dashboard_live_test.exs +++ b/test/towerops_web/live/dashboard_live_test.exs @@ -63,8 +63,8 @@ defmodule ToweropsWeb.DashboardLiveTest do {:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}") assert html =~ "Equipment" - assert html =~ "UP" - assert html =~ "DOWN" + assert html =~ "Up" + assert html =~ "Down" end test "displays active alerts count", %{conn: conn, organization: organization, site: site} do