fix(portal): sidebar active item state (#2119)

Adds `active_path` to determine whether or not to highlight a sidebar
item.

~~Leaving as draft for now to allow @devsnaked to contribute. Edit: Will
use this PR as the base for @devsnaked's upcoming changes~~

Edit: fixes #2065
This commit is contained in:
Jamil
2023-09-25 14:29:56 -07:00
committed by GitHub
parent 5d6dfc0c3a
commit 41bbf7e541
6 changed files with 224 additions and 18 deletions

View File

@@ -307,12 +307,6 @@ jobs:
options: --cap-add=IPC_LOCK
steps:
- uses: nanasess/setup-chromedriver@v2
with:
# XXX: This is an unfortunate workaround due to this issue:
# https://github.com/nanasess/setup-chromedriver/issues/199
# Still, it may not hurt to pin chromedriver and/or Chrome for more repeatable tests and
# possibly even matrix these to multiple versions to increase browser coverage.
chromedriver-version: '117.0.5938'
- uses: erlef/setup-beam@v1
with:
otp-version: "26.0.2"

View File

@@ -1,44 +1,75 @@
<.topbar subject={@subject} />
<.sidebar>
<.sidebar_item navigate={~p"/#{@account}/dashboard"} icon="hero-chart-bar-square-solid">
<.sidebar_item
current_path={@current_path}
navigate={~p"/#{@account}/dashboard"}
icon="hero-chart-bar-square-solid"
>
Dashboard
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/actors"} icon="hero-user-circle-solid">
<.sidebar_item
current_path={@current_path}
navigate={~p"/#{@account}/actors"}
icon="hero-user-circle-solid"
>
Actors
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/groups"} icon="hero-user-group-solid">
<.sidebar_item
current_path={@current_path}
navigate={~p"/#{@account}/groups"}
icon="hero-user-group-solid"
>
Groups
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/clients"} icon="hero-device-phone-mobile-solid">
<.sidebar_item
current_path={@current_path}
navigate={~p"/#{@account}/clients"}
icon="hero-device-phone-mobile-solid"
>
Clients
</.sidebar_item>
<.sidebar_item
current_path={@current_path}
navigate={~p"/#{@account}/gateway_groups"}
icon="hero-arrow-left-on-rectangle-solid"
>
Gateways
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/relay_groups"} icon="hero-arrows-right-left">
<.sidebar_item
current_path={@current_path}
navigate={~p"/#{@account}/relay_groups"}
icon="hero-arrows-right-left"
>
Relays
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/resources"} icon="hero-server-stack-solid">
<.sidebar_item
current_path={@current_path}
navigate={~p"/#{@account}/resources"}
icon="hero-server-stack-solid"
>
Resources
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/policies"} icon="hero-shield-check-solid">
<.sidebar_item
current_path={@current_path}
navigate={~p"/#{@account}/policies"}
icon="hero-shield-check-solid"
>
Policies
</.sidebar_item>
<.sidebar_item_group id="settings" icon="hero-cog-solid">
<.sidebar_item_group current_path={@current_path} id="settings" icon="hero-cog-solid">
<:name>Settings</:name>
<:item navigate={~p"/#{@account}/settings/account"}>Account</:item>
<:item navigate={~p"/#{@account}/settings/identity_providers"}>Identity Providers</:item>
<:item navigate={~p"/#{@account}/settings/identity_providers"}>
Identity Providers
</:item>
<:item navigate={~p"/#{@account}/settings/dns"}>DNS</:item>
</.sidebar_item_group>

View File

@@ -146,6 +146,8 @@ defmodule Web.NavigationComponents do
attr :icon, :string, required: true
attr :navigate, :string, required: true
slot :inner_block, required: true
attr :current_path, :string, required: true
attr :active_class, :string, required: false, default: "dark:bg-gray-700 bg-gray-100"
def sidebar_item(assigns) do
~H"""
@@ -154,6 +156,7 @@ defmodule Web.NavigationComponents do
flex items-center p-2
text-base font-medium text-gray-900
rounded-lg
#{String.starts_with?(@current_path, @navigate) && @active_class}
hover:bg-gray-100
dark:text-white dark:hover:bg-gray-700 group]}>
<.icon name={@icon} class={~w[
@@ -170,7 +173,8 @@ defmodule Web.NavigationComponents do
attr :id, :string, required: true, doc: "ID of the nav group container"
attr :icon, :string, required: true
# attr :navigate, :string, required: true
attr :current_path, :string, required: true
attr :active_class, :string, required: false, default: "dark:bg-gray-700 bg-gray-100"
slot :name, required: true
@@ -179,6 +183,13 @@ defmodule Web.NavigationComponents do
end
def sidebar_item_group(assigns) do
dropdown_hidden =
!Enum.any?(assigns.item, fn item ->
String.starts_with?(assigns.current_path, item.navigate)
end)
assigns = assign(assigns, dropdown_hidden: dropdown_hidden)
~H"""
<li>
<button
@@ -190,6 +201,7 @@ defmodule Web.NavigationComponents do
hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700]}
aria-controls={"dropdown-#{@id}"}
data-collapse-toggle={"dropdown-#{@id}"}
aria-hidden={@dropdown_hidden}
>
<.icon name={@icon} class={~w[
w-6 h-6 text-gray-500
@@ -203,11 +215,12 @@ defmodule Web.NavigationComponents do
group-hover:text-gray-900
dark:text-gray-400 dark:group-hover:text-white]} />
</button>
<ul id={"dropdown-#{@id}"}>
<ul id={"dropdown-#{@id}"} class={if @dropdown_hidden, do: "hidden", else: ""}>
<li :for={item <- @item}>
<.link navigate={item.navigate} class={~w[
flex items-center p-2 pl-11 w-full group rounded-lg
text-base font-medium text-gray-900
#{String.starts_with?(@current_path, item.navigate) && @active_class}
transition duration-75
hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700]}>
<%= render_slot(item) %>

View File

@@ -0,0 +1,12 @@
defmodule Web.Nav do
use Web, :verified_routes
def on_mount(:set_active_sidebar_item, _params, _session, socket) do
{:cont,
Phoenix.LiveView.attach_hook(socket, :current_path, :handle_params, &set_current_path/3)}
end
defp set_current_path(_params, uri, socket) do
{:cont, Phoenix.Component.assign(socket, :current_path, URI.parse(uri).path)}
end
end

View File

@@ -95,7 +95,8 @@ defmodule Web.Router do
Web.Sandbox,
{Web.Auth, :ensure_authenticated},
{Web.Auth, :ensure_account_admin_user_actor},
{Web.Auth, :mount_account}
{Web.Auth, :mount_account},
{Web.Nav, :set_active_sidebar_item}
] do
live "/dashboard", Dashboard

View File

@@ -0,0 +1,155 @@
defmodule Web.Live.Nav.SidebarTest do
use Web.ConnCase, async: true
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
%{
account: account,
actor: actor,
identity: identity
}
end
test "hides dropdown when path is not within dropdown children", %{
conn: conn,
account: account,
identity: identity
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/actors")
refute Enum.empty?(Floki.find(html, "ul#dropdown-settings.hidden"))
end
test "shows dropdown when path is within dropdown children", %{
conn: conn,
account: account,
identity: identity
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/settings/dns")
assert Enum.empty?(Floki.find(html, "ul#dropdown-settings.hidden"))
refute Enum.empty?(Floki.find(html, "ul#dropdown-settings"))
end
test "renders proper active sidebar item class for actors", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/actors")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/actors']")
assert String.trim(Floki.text(item)) == "Actors"
end
test "renders proper active sidebar item class for groups", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/groups")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/groups']")
assert String.trim(Floki.text(item)) == "Groups"
end
test "renders proper active sidebar item class for clients", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/clients")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/clients']")
assert String.trim(Floki.text(item)) == "Clients"
end
test "renders proper active sidebar item class for gateways", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/gateway_groups")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/gateway_groups']")
assert String.trim(Floki.text(item)) == "Gateways"
end
test "renders proper active sidebar item class for relays", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/relay_groups")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/relay_groups']")
assert String.trim(Floki.text(item)) == "Relays"
end
test "renders proper active sidebar item class for resources", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/resources")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/resources']")
assert String.trim(Floki.text(item)) == "Resources"
end
test "renders proper active sidebar item class for policies", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/policies")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/policies']")
assert String.trim(Floki.text(item)) == "Policies"
end
test "renders proper active sidebar item class for account", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/settings/account")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/settings/account']")
assert String.trim(Floki.text(item)) == "Account"
end
test "renders proper active sidebar item class for identity providers", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} =
live(authorize_conn(conn, identity), ~p"/#{account}/settings/identity_providers")
assert item =
Floki.find(html, "a.bg-gray-100[href='/#{account.id}/settings/identity_providers']")
assert String.trim(Floki.text(item)) == "Identity Providers"
end
test "renders proper active sidebar item class for new OIDC identity provider", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} =
live(
authorize_conn(conn, identity),
~p"/#{account}/settings/identity_providers/openid_connect/new"
)
assert item =
Floki.find(html, "a.bg-gray-100[href='/#{account.id}/settings/identity_providers']")
assert String.trim(Floki.text(item)) == "Identity Providers"
end
test "renders proper active sidebar item class for dns", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/settings/dns")
assert item = Floki.find(html, "a.bg-gray-100[href='/#{account.id}/settings/dns']")
assert String.trim(Floki.text(item)) == "DNS"
end
end