Packages
phoenix_kit
1.7.160
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/components/dashboard/admin_sidebar.ex
defmodule PhoenixKitWeb.Components.Dashboard.AdminSidebar do
@moduledoc """
Admin sidebar component for the PhoenixKit admin panel.
Renders the admin navigation using registry-driven Tab structs instead of
hardcoded HEEX. Supports:
- Permission-gated tabs (filtered by Registry)
- Module-enabled filtering (filtered by Registry)
- Dynamic children for Entities and Publishing
- Subtab expand/collapse
- Full reuse of the TabItem component for consistent rendering
## Usage
<.admin_sidebar
current_path={@current_path}
scope={@phoenix_kit_current_scope}
locale={@current_locale}
/>
"""
use Phoenix.Component
require Logger
alias PhoenixKit.Dashboard.{Group, Registry, Tab}
alias PhoenixKitWeb.Components.Dashboard.TabItem
import PhoenixKit.Dashboard.TabHelpers
import PhoenixKitWeb.Components.Core.Icon, only: [icon: 1]
@doc """
Renders the complete admin sidebar navigation.
## Attributes
- `current_path` - The current URL path for active state detection
- `scope` - The current authentication scope for permission filtering
- `locale` - The current locale for path generation
- `class` - Additional CSS classes
"""
attr :current_path, :string, default: "/admin"
attr :scope, :any, default: nil
attr :locale, :string, default: nil
attr :class, :string, default: ""
def admin_sidebar(assigns) do
# Get admin tabs, already filtered by level, permission, and module-enabled
# Expand dynamic children BEFORE active state so dynamic tabs get checked too
tabs =
:telemetry.span([:phoenix_kit, :admin_sidebar, :render], %{}, fn ->
result =
Registry.get_admin_tabs(scope: assigns.scope)
|> expand_dynamic_children(assigns.scope, assigns[:locale])
|> add_active_state(assigns.current_path)
{result, %{tab_count: length(result)}}
end)
# Group tabs
grouped_tabs = group_tabs(tabs)
groups = Registry.get_groups()
assigns =
assigns
|> assign(:tabs, tabs)
|> assign(:grouped_tabs, grouped_tabs)
|> assign(:groups, groups)
~H"""
<nav class={["space-y-2", @class]} role="navigation" aria-label="Admin navigation">
<%= for group <- sorted_groups(@groups, @grouped_tabs) do %>
<.admin_tab_group
group={group}
tabs={Map.get(@grouped_tabs, group.id, [])}
all_tabs={@tabs}
locale={@locale}
/>
<% end %>
<%!-- Render ungrouped tabs --%>
<%= for tab <- filter_top_level(Map.get(@grouped_tabs, nil, [])) do %>
<.admin_tab_with_subtabs
tab={tab}
all_tabs={@tabs}
locale={@locale}
/>
<% end %>
</nav>
"""
end
attr :group, :map, required: true
attr :tabs, :list, required: true
attr :all_tabs, :list, required: true
attr :locale, :string, default: nil
defp admin_tab_group(assigns) do
~H"""
<div class="space-y-1" data-group-id={@group.id}>
<%= if Group.localized_label(@group) do %>
<div class="px-3 py-2 text-xs font-semibold text-base-content/50 uppercase tracking-wider">
<span class="flex items-center gap-2">
<%= if @group.icon do %>
<.icon name={@group.icon} class="w-3.5 h-3.5" />
<% end %>
{Group.localized_label(@group)}
</span>
</div>
<% end %>
<%= for tab <- filter_top_level(@tabs) do %>
<.admin_tab_with_subtabs
tab={tab}
all_tabs={@all_tabs}
locale={@locale}
/>
<% end %>
</div>
"""
end
attr :tab, :any, required: true
attr :all_tabs, :list, required: true
attr :locale, :string, default: nil
defp admin_tab_with_subtabs(assigns) do
subtabs = get_subtabs_for(assigns.tab.id, assigns.all_tabs)
# Check all descendants (not just direct children) for active state
descendant_active = any_descendant_active?(assigns.tab.id, assigns.all_tabs)
show_subtabs =
Tab.show_subtabs?(assigns.tab, assigns.tab.active) or descendant_active
display_tab = maybe_redirect_to_first_subtab(assigns.tab, subtabs)
highlight_with_subtabs = Map.get(assigns.tab, :highlight_with_subtabs, false)
parent_active =
if descendant_active and not highlight_with_subtabs do
false
else
assigns.tab.active
end
assigns =
assigns
|> assign(:subtabs, subtabs)
|> assign(:show_subtabs, show_subtabs)
|> assign(:has_subtabs, subtabs != [])
|> assign(:display_tab, display_tab)
|> assign(:parent_active, parent_active)
~H"""
<div class="tab-with-subtabs" data-tab-id={@tab.id} data-has-subtabs={@has_subtabs}>
<TabItem.tab_item
tab={@display_tab}
active={@parent_active}
locale={@locale}
/>
<%= if @has_subtabs and @show_subtabs do %>
<div class="subtabs pl-1 border-l-2 border-base-300 ml-2 mt-1 space-y-0.5">
<%= for subtab <- @subtabs do %>
<.admin_subtab_item
subtab={subtab}
parent_tab={@tab}
all_tabs={@all_tabs}
locale={@locale}
/>
<% end %>
</div>
<% end %>
</div>
"""
end
attr :subtab, :any, required: true
attr :parent_tab, :any, required: true
attr :all_tabs, :list, required: true
attr :locale, :string, default: nil
defp admin_subtab_item(assigns) do
children = get_subtabs_for(assigns.subtab.id, assigns.all_tabs)
child_active = any_descendant_active?(assigns.subtab.id, assigns.all_tabs)
show_children =
children != [] and
(Tab.show_subtabs?(assigns.subtab, assigns.subtab.active) or child_active)
highlight_with_subtabs = Map.get(assigns.subtab, :highlight_with_subtabs, false)
subtab_active =
if child_active and not highlight_with_subtabs do
false
else
assigns.subtab.active
end
assigns =
assigns
|> assign(:children, children)
|> assign(:show_children, show_children)
|> assign(:subtab_active, subtab_active)
~H"""
<TabItem.tab_item
tab={@subtab}
active={@subtab_active}
locale={@locale}
parent_tab={@parent_tab}
/>
<%= if @show_children do %>
<div class="sub-subtabs pl-1 border-l-2 border-base-300 ml-2 mt-0.5 space-y-0.5">
<%= for child <- @children do %>
<TabItem.tab_item
tab={child}
active={child.active}
locale={@locale}
parent_tab={@subtab}
/>
<% end %>
</div>
<% end %>
"""
end
# --- Helpers ---
defp expand_dynamic_children(tabs, scope, locale) do
# Find tabs with dynamic_children (arity 1 or 2) and expand them.
# The 2-arity variant receives locale so modules can render translated
# child labels without falling back to `Gettext.get_locale/1`.
{parents_with_dynamic, other_tabs} =
Enum.split_with(tabs, fn tab ->
is_function(tab.dynamic_children, 1) or is_function(tab.dynamic_children, 2)
end)
dynamic_children =
Enum.flat_map(parents_with_dynamic, fn parent ->
children =
try do
invoke_dynamic_children(parent.dynamic_children, scope, locale)
rescue
error ->
Logger.warning(
"[AdminSidebar] dynamic_children for #{inspect(parent.id)} failed: #{Exception.message(error)}"
)
[]
end
# Ensure children have parent set, correct level, and resolved paths
Enum.map(children, fn child ->
child
|> Map.put(:parent, child.parent || parent.id)
|> Map.put(:level, :admin)
|> Tab.resolve_path(:admin)
end)
end)
# Active state is applied after this function by add_active_state/2
other_tabs ++ parents_with_dynamic ++ dynamic_children
end
# Dispatches on arity so modules can opt in to locale-aware rendering
# without breaking existing 1-arity `dynamic_children` implementations.
defp invoke_dynamic_children(fun, scope, locale) when is_function(fun, 2),
do: fun.(scope, locale)
defp invoke_dynamic_children(fun, scope, _locale) when is_function(fun, 1),
do: fun.(scope)
@doc false
# Test-only public delegate. The internal dispatch helper is `defp` because
# it shouldn't be part of the runtime API; this `@doc false` wrapper lets
# the unit suite exercise the actual arity dispatch (rather than just
# re-asserting Elixir's call semantics on anonymous functions). Not
# recommended for runtime callers — the contract is `dynamic_children_fn`,
# not this wrapper.
def __invoke_dynamic_children_for_test__(fun, scope, locale),
do: invoke_dynamic_children(fun, scope, locale)
# Recursively checks if any descendant (children, grandchildren, etc.) is active.
# Includes depth limit and cycle detection for safety with parent-app-registered tabs.
defp any_descendant_active?(parent_id, all_tabs, depth \\ 0, visited \\ %{})
defp any_descendant_active?(_parent_id, _all_tabs, depth, _visited) when depth > 5, do: false
defp any_descendant_active?(parent_id, all_tabs, depth, visited) do
if Map.has_key?(visited, parent_id) do
Logger.warning("[AdminSidebar] Circular tab reference detected: #{inspect(parent_id)}")
false
else
children = get_subtabs_for(parent_id, all_tabs)
new_visited = Map.put(visited, parent_id, true)
Enum.any?(children, fn child ->
child.active or any_descendant_active?(child.id, all_tabs, depth + 1, new_visited)
end)
end
end
defp maybe_redirect_to_first_subtab(%{redirect_to_first_subtab: true} = tab, [
first_subtab | _
]) do
%{tab | path: first_subtab.path}
end
defp maybe_redirect_to_first_subtab(tab, _subtabs), do: tab
end