Packages
phoenix_kit
1.7.31
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/dashboard/registry.ex
defmodule PhoenixKit.Dashboard.Registry do
@moduledoc """
Registry for managing dashboard tabs across the application.
The Registry provides both compile-time configuration via application config
and runtime registration for dynamic tabs. Tabs are stored in an ETS table
for efficient access.
## Configuration
Tabs can be configured in your application config:
config :phoenix_kit, :user_dashboard_tabs, [
%{
id: :orders,
label: "Orders",
icon: "hero-shopping-bag",
path: "/dashboard/orders",
priority: 100
},
%{
id: :settings,
label: "Settings",
icon: "hero-cog-6-tooth",
path: "/dashboard/settings",
priority: 900
}
]
## Runtime Registration
Parent applications can register tabs at runtime:
PhoenixKit.Dashboard.Registry.register(:my_app, [
Tab.new!(id: :custom, label: "Custom", path: "/dashboard/custom", priority: 150)
])
## Groups
Tabs can be organized into groups:
config :phoenix_kit, :user_dashboard_tab_groups, [
%{id: :main, label: nil, priority: 100},
%{id: :farm, label: "Farm Management", priority: 200, icon: "hero-cube"},
%{id: :account, label: "Account", priority: 900}
]
Then assign tabs to groups:
%{id: :printers, label: "Printers", path: "/dashboard/printers", group: :farm}
## PubSub Integration
The registry can broadcast tab updates:
PhoenixKit.Dashboard.Registry.update_tab_badge(:notifications, Badge.count(5))
LiveViews subscribed to "phoenix_kit:dashboard:tabs" will receive updates.
"""
use GenServer
alias PhoenixKit.Dashboard.{Badge, Tab}
alias PhoenixKit.PubSubHelper
# Suppress warnings about optional modules (loaded conditionally)
@compile {:no_warn_undefined, PhoenixKit.Modules.Tickets}
@compile {:no_warn_undefined, PhoenixKit.Modules.Billing}
@compile {:no_warn_undefined, PhoenixKit.Modules.Shop}
@ets_table :phoenix_kit_dashboard_tabs
@pubsub_topic "phoenix_kit:dashboard:tabs"
# Client API
@doc """
Starts the Registry GenServer.
This is typically called by the PhoenixKit supervisor.
"""
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Registers tabs for an application namespace.
## Examples
Registry.register(:my_app, [
Tab.new!(id: :home, label: "Home", path: "/dashboard", icon: "hero-home"),
Tab.new!(id: :orders, label: "Orders", path: "/dashboard/orders")
])
# Register a single tab
Registry.register(:my_app, Tab.new!(id: :settings, label: "Settings", path: "/dashboard/settings"))
"""
@spec register(atom(), Tab.t() | [Tab.t()]) :: :ok
def register(namespace, %Tab{} = tab) do
register(namespace, [tab])
end
def register(namespace, tabs) when is_atom(namespace) and is_list(tabs) do
GenServer.call(__MODULE__, {:register, namespace, tabs})
end
@doc """
Registers tabs from a map/keyword configuration.
Useful for registering tabs from config files.
## Examples
Registry.register_from_config(:my_app, [
%{id: :home, label: "Home", path: "/dashboard", icon: "hero-home"},
%{id: :orders, label: "Orders", path: "/dashboard/orders"}
])
"""
@spec register_from_config(atom(), [map()] | [keyword()]) :: :ok | {:error, term()}
def register_from_config(namespace, config) when is_atom(namespace) and is_list(config) do
tabs =
Enum.reduce_while(config, {:ok, []}, fn item, {:ok, acc} ->
case Tab.new(item) do
{:ok, tab} -> {:cont, {:ok, [tab | acc]}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
case tabs do
{:ok, tab_list} ->
register(namespace, Enum.reverse(tab_list))
:ok
{:error, reason} ->
{:error, reason}
end
end
@doc """
Unregisters all tabs for a namespace.
"""
@spec unregister(atom()) :: :ok
def unregister(namespace) when is_atom(namespace) do
GenServer.call(__MODULE__, {:unregister, namespace})
end
@doc """
Unregisters a specific tab by ID.
"""
@spec unregister_tab(atom()) :: :ok
def unregister_tab(tab_id) when is_atom(tab_id) do
GenServer.call(__MODULE__, {:unregister_tab, tab_id})
end
@doc """
Gets all registered tabs, sorted by priority.
## Options
- `:scope` - The current scope (for visibility filtering)
- `:path` - The current path (for active state detection)
- `:include_hidden` - Include tabs that would be hidden (default: false)
## Examples
Registry.get_tabs()
Registry.get_tabs(scope: socket.assigns.phoenix_kit_current_scope)
"""
@spec get_tabs(keyword()) :: [Tab.t()]
def get_tabs(opts \\ []) do
scope = opts[:scope]
include_hidden = opts[:include_hidden] || false
all_tabs()
|> maybe_filter_visibility(scope, include_hidden)
|> sort_tabs()
end
@doc """
Gets a specific tab by ID.
"""
@spec get_tab(atom()) :: Tab.t() | nil
def get_tab(tab_id) when is_atom(tab_id) do
if initialized?() do
case :ets.lookup(@ets_table, {:tab, tab_id}) do
[{_, tab}] -> tab
[] -> nil
end
else
nil
end
end
@doc """
Gets all tabs in a specific group.
"""
@spec get_tabs_in_group(atom(), keyword()) :: [Tab.t()]
def get_tabs_in_group(group_id, opts \\ []) do
get_tabs(opts)
|> Enum.filter(&(&1.group == group_id))
end
@doc """
Gets all subtabs for a given parent tab ID.
## Examples
Registry.get_subtabs(:orders)
# => [%Tab{id: :pending_orders, parent: :orders, ...}, ...]
"""
@spec get_subtabs(atom(), keyword()) :: [Tab.t()]
def get_subtabs(parent_id, opts \\ []) when is_atom(parent_id) do
get_tabs(opts)
|> Enum.filter(&(&1.parent == parent_id))
end
@doc """
Gets only top-level tabs (tabs without a parent).
## Options
Same as `get_tabs/1`.
## Examples
Registry.get_top_level_tabs()
# => [%Tab{id: :orders, parent: nil, ...}, ...]
"""
@spec get_top_level_tabs(keyword()) :: [Tab.t()]
def get_top_level_tabs(opts \\ []) do
get_tabs(opts)
|> Enum.filter(&Tab.top_level?/1)
end
@doc """
Checks if a tab has any subtabs.
## Examples
Registry.has_subtabs?(:orders)
# => true
"""
@spec has_subtabs?(atom()) :: boolean()
def has_subtabs?(tab_id) when is_atom(tab_id) do
get_subtabs(tab_id) |> Enum.any?()
end
@doc """
Gets all registered groups, sorted by priority.
"""
@spec get_groups() :: [map()]
def get_groups do
if initialized?() do
case :ets.lookup(@ets_table, :groups) do
[{:groups, groups}] -> Enum.sort_by(groups, & &1.priority)
[] -> []
end
else
[]
end
end
@doc """
Registers tab groups.
## Examples
Registry.register_groups([
%{id: :main, label: nil, priority: 100},
%{id: :farm, label: "Farm Management", priority: 200},
%{id: :account, label: "Account", priority: 900}
])
"""
@spec register_groups([map()]) :: :ok
def register_groups(groups) when is_list(groups) do
GenServer.call(__MODULE__, {:register_groups, groups})
end
@doc """
Updates a tab's badge.
This broadcasts an update to all subscribed LiveViews.
## Examples
Registry.update_tab_badge(:notifications, Badge.count(5))
Registry.update_tab_badge(:printers, Badge.count(3, color: :warning))
"""
@spec update_tab_badge(atom(), Badge.t() | map() | nil) :: :ok
def update_tab_badge(tab_id, badge) do
GenServer.call(__MODULE__, {:update_badge, tab_id, badge})
end
@doc """
Sets an attention animation on a tab.
## Examples
Registry.set_tab_attention(:alerts, :pulse)
Registry.set_tab_attention(:notifications, :bounce)
"""
@spec set_tab_attention(atom(), atom()) :: :ok
def set_tab_attention(tab_id, attention)
when attention in [nil, :pulse, :bounce, :shake, :glow] do
GenServer.call(__MODULE__, {:set_attention, tab_id, attention})
end
@doc """
Clears attention animation from a tab.
"""
@spec clear_tab_attention(atom()) :: :ok
def clear_tab_attention(tab_id) do
set_tab_attention(tab_id, nil)
end
@doc """
Gets the PubSub topic for tab updates.
LiveViews can subscribe to this topic to receive real-time tab updates.
## Example
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(PubSubHelper.pubsub(), Registry.pubsub_topic())
end
{:ok, socket}
end
def handle_info({:tab_updated, tab}, socket) do
# Handle tab update
{:noreply, socket}
end
"""
@spec pubsub_topic() :: String.t()
def pubsub_topic, do: @pubsub_topic
@doc """
Broadcasts a tab update to all subscribers.
"""
@spec broadcast_update(Tab.t()) :: :ok
def broadcast_update(%Tab{} = tab) do
Phoenix.PubSub.broadcast(PubSubHelper.pubsub(), @pubsub_topic, {:tab_updated, tab})
:ok
rescue
_ -> :ok
end
@doc """
Broadcasts a full tab list refresh to all subscribers.
"""
@spec broadcast_refresh() :: :ok
def broadcast_refresh do
Phoenix.PubSub.broadcast(PubSubHelper.pubsub(), @pubsub_topic, :tabs_refreshed)
:ok
rescue
_ -> :ok
end
@doc """
Checks if the registry has been initialized.
"""
@spec initialized?() :: boolean()
def initialized? do
case :ets.info(@ets_table) do
:undefined -> false
_ -> true
end
end
@doc """
Gets all tabs with their active state for the given path.
Returns tabs with an additional `:active` key set based on path matching.
"""
@spec get_tabs_with_active(String.t(), keyword()) :: [map()]
def get_tabs_with_active(current_path, opts \\ []) do
get_tabs(opts)
|> Enum.map(fn tab ->
Map.put(tab, :active, Tab.matches_path?(tab, current_path))
end)
end
@doc """
Loads the default PhoenixKit tabs (Dashboard, Settings).
Called during initialization and can be used to reset to defaults.
"""
@spec load_defaults() :: :ok
def load_defaults do
GenServer.call(__MODULE__, :load_defaults)
end
@doc """
Loads tabs from application configuration.
Reads from `:phoenix_kit, :user_dashboard_tabs` config key.
"""
@spec load_from_config() :: :ok
def load_from_config do
GenServer.call(__MODULE__, :load_from_config)
end
# GenServer Callbacks
@impl true
def init(_opts) do
# Create ETS table for tab storage
:ets.new(@ets_table, [:named_table, :set, :public, read_concurrency: true])
# Load defaults and config
load_defaults_internal()
load_from_config_internal()
{:ok, %{namespaces: MapSet.new([:phoenix_kit])}}
end
@impl true
def handle_call({:register, namespace, tabs}, _from, state) do
Enum.each(tabs, fn tab ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, namespace, tab.id}, true})
end)
broadcast_refresh()
{:reply, :ok, %{state | namespaces: MapSet.put(state.namespaces, namespace)}}
end
@impl true
def handle_call({:unregister, namespace}, _from, state) do
# Find and remove all tabs for this namespace
pattern = {{:namespace, namespace, :_}, :_}
:ets.match_object(@ets_table, pattern)
|> Enum.each(fn {{:namespace, ^namespace, tab_id}, _} ->
:ets.delete(@ets_table, {:tab, tab_id})
:ets.delete(@ets_table, {:namespace, namespace, tab_id})
end)
broadcast_refresh()
{:reply, :ok, %{state | namespaces: MapSet.delete(state.namespaces, namespace)}}
end
@impl true
def handle_call({:unregister_tab, tab_id}, _from, state) do
:ets.delete(@ets_table, {:tab, tab_id})
# Remove from all namespace mappings
:ets.match_delete(@ets_table, {{:namespace, :_, tab_id}, :_})
broadcast_refresh()
{:reply, :ok, state}
end
@impl true
def handle_call({:register_groups, groups}, _from, state) do
:ets.insert(@ets_table, {:groups, groups})
broadcast_refresh()
{:reply, :ok, state}
end
@impl true
def handle_call({:update_badge, tab_id, badge}, _from, state) do
case get_tab(tab_id) do
nil ->
{:reply, :ok, state}
tab ->
updated_tab = Tab.update_badge(tab, badge)
:ets.insert(@ets_table, {{:tab, tab_id}, updated_tab})
broadcast_update(updated_tab)
{:reply, :ok, state}
end
end
@impl true
def handle_call({:set_attention, tab_id, attention}, _from, state) do
case get_tab(tab_id) do
nil ->
{:reply, :ok, state}
tab ->
updated_tab = Tab.set_attention(tab, attention)
:ets.insert(@ets_table, {{:tab, tab_id}, updated_tab})
broadcast_update(updated_tab)
{:reply, :ok, state}
end
end
@impl true
def handle_call(:load_defaults, _from, state) do
load_defaults_internal()
broadcast_refresh()
{:reply, :ok, state}
end
@impl true
def handle_call(:load_from_config, _from, state) do
load_from_config_internal()
broadcast_refresh()
{:reply, :ok, state}
end
# Private helpers
defp all_tabs do
:ets.match_object(@ets_table, {{:tab, :_}, :_})
|> Enum.map(fn {_, tab} -> tab end)
rescue
_ -> []
end
defp maybe_filter_visibility(tabs, nil, _include_hidden), do: tabs
defp maybe_filter_visibility(tabs, _scope, true), do: tabs
defp maybe_filter_visibility(tabs, scope, false) do
Enum.filter(tabs, &Tab.visible?(&1, scope))
end
defp sort_tabs(tabs) do
Enum.sort_by(tabs, & &1.priority)
end
defp clear_namespace_tabs(namespace) do
# Find and remove all tabs for this namespace
pattern = {{:namespace, namespace, :_}, :_}
:ets.match_object(@ets_table, pattern)
|> Enum.each(fn {{:namespace, ^namespace, tab_id}, _} ->
:ets.delete(@ets_table, {:tab, tab_id})
:ets.delete(@ets_table, {:namespace, namespace, tab_id})
end)
rescue
_ -> :ok
end
defp load_defaults_internal do
# Clear existing phoenix_kit namespace tabs first
clear_namespace_tabs(:phoenix_kit)
# Default PhoenixKit tabs
defaults = [
Tab.new!(
id: :dashboard_home,
label: "Dashboard",
icon: "hero-home",
path: "/dashboard",
priority: 100,
match: :exact,
group: :main
),
Tab.new!(
id: :dashboard_settings,
label: "Settings",
icon: "hero-cog-6-tooth",
path: "/dashboard/settings",
priority: 900,
match: :prefix,
group: :account
)
]
# Add tickets tab if module is enabled (checking at runtime)
defaults =
if tickets_enabled?() do
ticket_tab =
Tab.new!(
id: :dashboard_tickets,
label: "My Tickets",
icon: "hero-ticket",
path: "/dashboard/tickets",
priority: 800,
match: :prefix,
group: :account
)
defaults ++ [ticket_tab]
else
defaults
end
# Add billing tabs if module is enabled
defaults =
if billing_enabled?() do
billing_tabs = [
Tab.new!(
id: :dashboard_orders,
label: "My Orders",
icon: "hero-shopping-bag",
path: "/dashboard/orders",
priority: 200,
match: :prefix,
group: :main
),
Tab.new!(
id: :dashboard_billing_profiles,
label: "Billing Profiles",
icon: "hero-identification",
path: "/dashboard/billing-profiles",
priority: 850,
match: :prefix,
group: :account
)
]
defaults ++ billing_tabs
else
defaults
end
# Add shop tabs if module is enabled
defaults =
if shop_enabled?() do
shop_tabs = [
Tab.new!(
id: :dashboard_shop,
label: "Shop",
icon: "hero-building-storefront",
path: "/shop",
priority: 300,
match: :prefix,
group: :shop
),
Tab.new!(
id: :dashboard_cart,
label: "My Cart",
icon: "hero-shopping-cart",
path: "/cart",
priority: 310,
match: :prefix,
group: :shop
)
]
defaults ++ shop_tabs
else
defaults
end
# Default groups
groups = [
%{id: :main, label: nil, priority: 100},
%{id: :shop, label: nil, priority: 200},
%{id: :account, label: nil, priority: 900}
]
Enum.each(defaults, fn tab ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, :phoenix_kit, tab.id}, true})
end)
:ets.insert(@ets_table, {:groups, groups})
end
defp load_from_config_internal do
# Load from user_dashboard_categories (admin-style config)
load_from_categories_config()
# Load tab configuration (flat list style)
case Application.get_env(:phoenix_kit, :user_dashboard_tabs) do
nil ->
:ok
tabs when is_list(tabs) ->
Enum.each(tabs, fn tab_config ->
case Tab.new(tab_config) do
{:ok, tab} ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, :config, tab.id}, true})
{:error, _reason} ->
# Log error but continue
:ok
end
end)
end
# Load group configuration
case Application.get_env(:phoenix_kit, :user_dashboard_tab_groups) do
nil ->
:ok
groups when is_list(groups) ->
:ets.insert(@ets_table, {:groups, groups})
end
end
# Load from user_dashboard_categories config (admin-style format)
defp load_from_categories_config do
alias PhoenixKit.Config.UserDashboardCategories
categories = UserDashboardCategories.get_categories()
if categories != [] do
# Convert categories to tabs and register them
tabs = UserDashboardCategories.to_tabs(categories)
Enum.each(tabs, fn tab_config ->
case Tab.new(tab_config) do
{:ok, tab} ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, :categories, tab.id}, true})
{:error, _reason} ->
# Log error but continue
:ok
end
end)
# Convert categories to groups and register them
groups = UserDashboardCategories.to_groups(categories)
existing_groups = get_groups()
merged_groups = merge_groups(existing_groups, groups)
:ets.insert(@ets_table, {:groups, merged_groups})
end
end
# Merge groups, preferring existing groups with same ID
defp merge_groups(existing, new) do
existing_ids = MapSet.new(Enum.map(existing, & &1.id))
new_groups =
Enum.reject(new, fn group ->
MapSet.member?(existing_ids, group.id)
end)
(existing ++ new_groups)
|> Enum.sort_by(& &1.priority)
end
defp tickets_enabled? do
Code.ensure_loaded?(PhoenixKit.Modules.Tickets) and
function_exported?(PhoenixKit.Modules.Tickets, :enabled?, 0) and
call_tickets_enabled()
rescue
_ -> false
end
defp billing_enabled? do
Code.ensure_loaded?(PhoenixKit.Modules.Billing) and
function_exported?(PhoenixKit.Modules.Billing, :enabled?, 0) and
call_billing_enabled()
rescue
_ -> false
end
defp shop_enabled? do
Code.ensure_loaded?(PhoenixKit.Modules.Shop) and
function_exported?(PhoenixKit.Modules.Shop, :enabled?, 0) and
call_shop_enabled()
rescue
_ -> false
end
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
defp call_tickets_enabled, do: PhoenixKit.Modules.Tickets.enabled?()
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
defp call_billing_enabled, do: PhoenixKit.Modules.Billing.enabled?()
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
defp call_shop_enabled, do: PhoenixKit.Modules.Shop.enabled?()
end