Packages

phoenix_kit

1.7.91
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
phoenix_kit lib phoenix_kit dashboard ADMIN_README.md
Raw

lib/phoenix_kit/dashboard/ADMIN_README.md

# PhoenixKit Admin Navigation System
Registry-driven admin sidebar navigation that replaces hardcoded HEEX with configurable, permission-gated Tab structs. Shares the same underlying registry and rendering infrastructure as the [User Dashboard Tab System](README.md).
## How It Works
All admin navigation items are registered as Tab structs in the Dashboard Registry with `level: :admin`. The admin sidebar component reads these tabs, filters by permission and module-enabled status, and renders them using the same `TabItem` component as the user dashboard.
### Three-Layer Visibility
Every admin tab passes through three filters before rendering:
1. **Module Enabled** — Is the feature module active? (e.g., is Billing enabled?)
2. **Permission Granted** — Does the user's role have access? (checked via `Scope.has_module_access?/2`)
3. **Custom Visibility** — Optional `visible` function for special logic
```
Tab registered → module_enabled? → permission_granted? → visible? → rendered
```
## Default Admin Tabs
PhoenixKit registers ~50 admin tabs automatically on startup, organized into three groups:
| Group | Tabs |
|-------|------|
| **Main** | Dashboard, Users (+ 6 subtabs), Media |
| **Modules** | Emails, Billing, Shop, Entities, AI, Sync, DB, Posts, Comments, Publishing, Jobs, Tickets, Modules |
| **System** | Settings (+ ~20 subtabs covering all module settings) |
Each tab has a `permission` field matching one of the 25 permission keys (e.g., `"billing"`, `"users"`, `"settings"`). Tabs for disabled modules are automatically hidden.
## Customizing Admin Tabs
### Adding Tabs via Config
Add custom tabs to the admin sidebar:
```elixir
# config/config.exs
config :phoenix_kit, :admin_dashboard_tabs, [
%{
id: :admin_analytics,
label: "Analytics",
icon: "hero-chart-bar",
path: "/admin/analytics",
permission: "dashboard",
priority: 350,
group: :admin_main
}
]
```
### Adding Tabs with Seamless Navigation
By default, custom tabs are sidebar links only — the parent app must define the actual LiveView routes. If those routes are in a different `live_session`, navigation causes a full page reload.
To avoid this, add the `live_view` field. PhoenixKit will auto-generate the route inside its shared admin `live_session`, giving you seamless LiveView navigation:
```elixir
config :phoenix_kit, :admin_dashboard_tabs, [
%{
id: :admin_analytics,
label: "Analytics",
icon: "hero-chart-bar",
path: "/admin/analytics",
permission: "dashboard",
priority: 350,
group: :admin_main,
live_view: {MyAppWeb.AnalyticsLive, :index} # Auto-generates route
}
]
```
With `live_view` set, PhoenixKit:
- Generates `live "/admin/analytics", MyAppWeb.AnalyticsLive, :index` inside the admin `live_session`
- Applies the `:phoenix_kit_ensure_admin` on_mount hook automatically
- Navigation from other admin pages uses LiveView `navigate` (no full page reload)
**Without `live_view`**: Parent app defines routes in its own router (may be a different `live_session`).
### Tab Fields Reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `id` | atom | required | Unique identifier (prefix with `admin_` by convention) |
| `label` | string | required | Display text in sidebar |
| `icon` | string | nil | Heroicon name (e.g., `"hero-chart-bar"`) |
| `path` | string | required | URL path without prefix (e.g., `"/admin/analytics"`) |
| `priority` | integer | 500 | Sort order (lower = higher in sidebar) |
| `level` | atom | `:admin` | Set automatically by config loader |
| `permission` | string | nil | Permission key for access control (e.g., `"billing"`) |
| `group` | atom | nil | Group ID: `:admin_main`, `:admin_modules`, or `:admin_system` |
| `parent` | atom | nil | Parent tab ID for subtab relationships |
| `match` | atom | `:prefix` | Path matching: `:exact`, `:prefix`, or `{:regex, ~r/...}` |
| `visible` | function | nil | `(scope -> boolean)` for non-permission conditional logic (feature flags, user data). For access control, use `permission` instead. |
| `live_view` | tuple | nil | `{Module, :action}` to auto-generate a route |
| `subtab_display` | atom | `:when_active` | `:when_active` or `:always` |
| `highlight_with_subtabs` | boolean | false | Highlight parent when subtab is active |
| `dynamic_children` | function | nil | `(scope -> [Tab.t()])` for runtime subtabs |
### Modifying Default Tabs
Update or remove default tabs at runtime:
```elixir
# Change a default tab's label or icon
PhoenixKit.Dashboard.update_tab(:admin_dashboard, %{label: "Home", icon: "hero-home"})
# Remove a default tab
PhoenixKit.Dashboard.unregister_tab(:admin_jobs)
```
### Registering Tabs at Runtime
```elixir
# Register admin tabs programmatically (level: :admin is set automatically)
PhoenixKit.Dashboard.register_admin_tabs(:my_app, [
%{
id: :admin_analytics,
label: "Analytics",
icon: "hero-chart-bar",
path: "/admin/analytics",
permission: "dashboard",
priority: 350,
group: :admin_main
}
])
# Unregister all tabs for a namespace
PhoenixKit.Dashboard.unregister_tabs(:my_app)
```
## Subtabs
Admin tabs support parent/child relationships, working the same as [user dashboard subtabs](README.md#subtabs):
```elixir
config :phoenix_kit, :admin_dashboard_tabs, [
# Parent
%{
id: :admin_reports,
label: "Reports",
icon: "hero-document-chart-bar",
path: "/admin/reports",
permission: "dashboard",
priority: 360,
group: :admin_main,
subtab_display: :when_active,
live_view: {MyAppWeb.ReportsLive, :index}
},
# Subtabs
%{
id: :admin_reports_sales,
label: "Sales",
path: "/admin/reports/sales",
parent: :admin_reports,
priority: 361,
live_view: {MyAppWeb.ReportsSalesLive, :index}
},
%{
id: :admin_reports_users,
label: "Users",
path: "/admin/reports/users",
parent: :admin_reports,
priority: 362,
live_view: {MyAppWeb.ReportsUsersLive, :index}
}
]
```
## Dynamic Children
Some admin tabs generate subtabs at render time based on data:
- **Entities** — A subtab for each published entity type
- **Publishing** — A subtab for each publishing group from settings
These use the `dynamic_children` field — a function `(scope -> [Tab.t()])` called when the sidebar renders. Dynamic children are always rendered under their parent tab and inherit its permission.
### Custom Dynamic Children
```elixir
PhoenixKit.Dashboard.register_admin_tabs(:my_app, [
%{
id: :admin_workspaces,
label: "Workspaces",
icon: "hero-squares-2x2",
path: "/admin/workspaces",
permission: "dashboard",
priority: 400,
group: :admin_main,
dynamic_children: fn _scope ->
MyApp.Workspaces.list_active()
|> Enum.with_index()
|> Enum.map(fn {ws, idx} ->
%PhoenixKit.Dashboard.Tab{
id: :"admin_workspace_#{ws.slug}",
label: ws.name,
icon: "hero-square-2-stack",
path: "/admin/workspaces/#{ws.slug}",
priority: 401 + idx,
level: :admin,
permission: "dashboard",
match: :prefix,
parent: :admin_workspaces
}
end)
end
}
])
```
**Performance note**: Dynamic children functions run on every sidebar render (each navigation). Keep them fast — use cached data, avoid expensive queries.
## Permission System
Admin tabs integrate with PhoenixKit's module-level permissions (`PhoenixKit.Users.Permissions`):
- **Owner** — Always has full access (hardcoded, no DB rows needed)
- **Admin** — Gets all 25 built-in permissions by default
- **Custom roles** — Start with no permissions; grant via matrix UI or API
### Built-in Permission Keys
The `permission` field on a tab can use any of the 25 built-in keys:
**Core (always enabled):** `dashboard`, `users`, `media`, `settings`, `modules`
**Feature modules (enabled/disabled):** `billing`, `shop`, `emails`, `entities`, `tickets`, `posts`, `comments`, `ai`, `sync`, `publishing`, `referrals`, `sitemap`, `seo`, `maintenance`, `storage`, `languages`, `connections`, `legal`, `db`, `jobs`
When a tab's `permission` points to a feature module:
- If the module is **disabled**, the tab is hidden for everyone
- If the module is **enabled**, the tab is shown only to users whose role has that permission
### Custom Permission Keys (Auto-Registration)
When a custom admin tab uses a permission key that isn't one of the 25 built-in keys, PhoenixKit **automatically registers it** as a custom permission. The key appears in the permission matrix and roles popup under an **Custom** section, where it can be granted or revoked per role — just like built-in permissions.
```elixir
config :phoenix_kit, :admin_dashboard_tabs, [
%{
id: :admin_analytics,
label: "Analytics",
icon: "hero-chart-bar",
path: "/admin/analytics",
permission: "analytics", # Not a built-in key → auto-registered
group: :admin_main,
live_view: {MyAppWeb.AnalyticsLive, :index}
}
]
```
**What happens automatically:**
1. `"analytics"` is registered as a custom permission key with label and icon from the tab config
2. It appears in the permission matrix and roles popup under **Custom**
3. Owner gets automatic access (Owner always gets all keys, including custom ones)
4. The tab is treated as "always enabled" (custom keys have no module toggle)
5. The LiveView module → permission mapping is cached for auth enforcement on mount
**Custom keys must** match `~r/^[a-z][a-z0-9_]*$/`. Using a built-in key name raises `ArgumentError`.
### Subtab Permission Inheritance
Subtabs inherit access from their parent tab's permission. When a parent tab is hidden (user lacks its permission), all its subtabs are hidden too — no separate permission needed:
```elixir
config :phoenix_kit, :admin_dashboard_tabs, [
# Parent — requires "analytics" permission
%{
id: :admin_analytics,
label: "Analytics",
icon: "hero-chart-bar",
path: "/admin/analytics",
permission: "analytics",
priority: 350,
group: :admin_main,
live_view: {MyAppWeb.AnalyticsLive, :index}
},
# Subtabs — no permission field needed, inherit from parent
%{
id: :admin_analytics_sales,
label: "Sales",
path: "/admin/analytics/sales",
parent: :admin_analytics,
priority: 351,
live_view: {MyAppWeb.AnalyticsSalesLive, :index}
},
%{
id: :admin_analytics_traffic,
label: "Traffic",
path: "/admin/analytics/traffic",
parent: :admin_analytics,
priority: 352,
live_view: {MyAppWeb.AnalyticsTrafficLive, :index}
}
]
```
If a subtab needs its own independent permission, it can set a `permission` field — this will auto-register a separate custom key:
```elixir
%{
id: :admin_analytics_billing,
label: "Billing Reports",
path: "/admin/analytics/billing",
parent: :admin_analytics,
permission: "analytics_billing", # Separate permission, auto-registered
priority: 353
}
```
### Programmatic Registration
Custom permission keys can also be registered directly, independent of tabs:
```elixir
PhoenixKit.Users.Permissions.register_custom_key("analytics",
label: "Analytics",
icon: "hero-chart-bar",
description: "Analytics dashboard and reports"
)
```
### Granting Custom Permissions
Custom permissions work exactly like built-in ones:
```elixir
# Via API
Permissions.grant_permission(role_uuid, "analytics", granted_by_uuid)
# Via set_permissions (includes custom keys)
Permissions.set_permissions(role_uuid, ["dashboard", "users", "analytics"], granted_by_uuid)
# Grant all (includes custom keys)
Permissions.grant_all_permissions(role_uuid, granted_by_uuid)
```
Or use the admin UI: navigate to the permission matrix or the role's permission editor — custom keys appear under the **Custom** section.
## Navigation Architecture
### LiveView Sessions
All PhoenixKit admin routes share a single `live_session`:
```elixir
live_session :phoenix_kit_admin,
on_mount: [{PhoenixKitWeb.Users.Auth, :phoenix_kit_ensure_admin}] do
# All admin routes — PhoenixKit core + modules + custom (with live_view)
end
```
This means:
- Navigating between admin pages uses LiveView `navigate` (WebSocket stays alive)
- Each page does a lightweight MOUNT (expected behavior for different LiveView modules)
- No full page reloads within the admin panel
**Important**: Custom admin routes defined by the parent app WITHOUT `live_view` may be in a different `live_session`, which would cause a full page reload when navigating to them. Use `live_view` in your tab config to avoid this.
### Tab Rendering Flow
```
1. Registry.get_admin_tabs(scope: scope)
├── Filter by level (:admin + :all)
├── Filter by module enabled (deduplicated per permission key)
├── Filter by permission (in-memory MapSet check)
└── Filter by visibility (custom functions)
2. AdminSidebar component
├── Expand dynamic children (entities, publishing)
├── Add active state based on current_path
├── Group tabs by group field
└── Render via TabItem component (shared with user dashboard)
```
**Important**: Dynamic children are expanded *before* active state is applied, so that dynamically-generated subtabs (e.g., individual entity types) correctly highlight when navigated to.
## API Reference
```elixir
# Admin-specific
PhoenixKit.Dashboard.get_admin_tabs(opts) # Get filtered admin tabs
PhoenixKit.Dashboard.get_user_tabs(opts) # Get filtered user tabs
PhoenixKit.Dashboard.register_admin_tabs(ns, tabs) # Register with level: :admin
PhoenixKit.Dashboard.update_tab(tab_id, attrs) # Modify existing tab
PhoenixKit.Dashboard.load_admin_defaults() # Reload default admin tabs
# All standard Dashboard APIs also work (see README.md)
PhoenixKit.Dashboard.unregister_tab(tab_id)
PhoenixKit.Dashboard.get_tab(tab_id)
# etc.
```
## File Structure
```
lib/phoenix_kit/dashboard/
├── admin_tabs.ex # Default admin tab definitions (~50 tabs)
├── dashboard.ex # Public API facade
├── registry.ex # Tab registry GenServer (shared user + admin)
├── tab.ex # Tab struct with level/permission/dynamic_children fields
├── ADMIN_README.md # This file
└── README.md # User dashboard documentation
lib/phoenix_kit_web/components/dashboard/
├── admin_sidebar.ex # Admin sidebar component
├── sidebar.ex # User dashboard sidebar component
├── tab_item.ex # Shared tab rendering component
└── ...
```
## Creating Custom Admin Pages
When using the `live_view` field, your LiveView runs inside PhoenixKit's admin `live_session` and must use the admin layout. Here's the complete pattern:
### 1. Create the LiveView
```elixir
# lib/my_app_web/phoenix_kit_live/admin_analytics_live.ex
defmodule MyAppWeb.PhoenixKitLive.AdminAnalyticsLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, page_title: "Analytics")}
end
def render(assigns) do
~H"""
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
page_title={@page_title}
current_path={@url_path}
phoenix_kit_current_scope={@phoenix_kit_current_scope}
current_locale={assigns[:current_locale]}
>
<div class="container flex-col mx-auto px-4 py-6">
<h1 class="text-2xl font-bold mb-6">Analytics Dashboard</h1>
<%!-- Your content here --%>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>
"""
end
end
```
### 2. Register the Tab
```elixir
# config/config.exs
config :phoenix_kit, :admin_dashboard_tabs, [
%{
id: :admin_analytics,
label: "Analytics",
icon: "hero-chart-bar",
path: "/admin/analytics",
permission: "dashboard",
priority: 150,
group: :admin_main,
live_view: {MyAppWeb.PhoenixKitLive.AdminAnalyticsLive, :index}
}
]
```
### Key Points
- **Use `@url_path` not `@current_path`** — The `url_path` assign is set by PhoenixKit's `on_mount` hooks. There is no `current_path` assign.
- **Use `LayoutWrapper.app_layout`** — This is the admin layout with the admin sidebar. Do NOT use `Layouts.dashboard` (that's the user dashboard layout).
- **Don't pass `project_title`** — The `app_layout` component has a built-in default; passing it from the LiveView will crash since it's not in the assigns.
- **Use `assigns[:current_locale]`** — Use bracket access for optional assigns that may not be set.
- **Place LiveViews under `phoenix_kit_live/`** — Convention for LiveViews that run inside PhoenixKit's admin `live_session`.
### Available Assigns
These assigns are automatically set by PhoenixKit's `on_mount` hooks in the admin `live_session`:
| Assign | Type | Description |
|--------|------|-------------|
| `@url_path` | string | Current URL path (use for `current_path` in layout) |
| `@phoenix_kit_current_scope` | Scope.t() | Auth scope with user, roles, and permissions |
| `@phoenix_kit_current_user` | User.t() | Current authenticated user |
| `@current_locale` | string | Current locale code (may be nil) |
| `@flash` | map | Flash messages |
| `@live_action` | atom | The action from the route (e.g., `:index`) |
| `@show_maintenance` | boolean | Whether maintenance mode banner is shown |
## Legacy Config Compatibility
The legacy `AdminDashboardCategories` config format is still supported but deprecated:
```elixir
# Legacy format (deprecated, will log warning)
config :phoenix_kit, AdminDashboardCategories, [
%{title: "Custom", icon: "hero-star", tabs: [
%{title: "Analytics", url: "/admin/analytics", icon: "hero-chart-bar"}
]}
]
# New format (recommended)
config :phoenix_kit, :admin_dashboard_tabs, [
%{id: :admin_analytics, label: "Analytics", icon: "hero-chart-bar",
path: "/admin/analytics", permission: "dashboard", group: :admin_main}
]
```
Legacy categories are automatically converted to admin Tab structs at startup. A deprecation warning is logged when legacy config is detected.
## Important: Compile-Time Behavior
The `live_view` field is evaluated **at compile time**. Routes for custom admin tabs are generated during compilation of the router.
### What this means
1. The LiveView module referenced in `live_view` must exist and compile successfully
2. If the module doesn't compile, the route is silently skipped (a warning is emitted)
3. Routes are baked into the compiled router — they won't update until recompilation
### After changing `:admin_dashboard_tabs` config
```bash
mix compile --force
```
Without `--force`, the router may not recompile and your tab changes won't take effect.
### Troubleshooting
**"My custom tab appears in the sidebar but links to a 404"**
- The LiveView module may not have been compiled when the router compiled
- Run `mix compile --force` to regenerate routes
**"My custom tab doesn't appear at all"**
1. Verify the tab config is correct (has `id`, `label`, `path`, `permission`)
2. Check that the module is enabled (if permission maps to a feature module)
3. Check that the user's role has the required permission
4. Check `mix compile --force` was run after config changes
**"Navigation causes a full page reload"**
- The tab is missing the `live_view` field, so PhoenixKit can't generate a route in its admin `live_session`
- Add `live_view: {MyModule, :index}` to enable seamless navigation
## Telemetry
The admin sidebar emits telemetry events for performance monitoring:
- `[:phoenix_kit, :admin_sidebar, :render, :start]` — emitted when sidebar rendering begins
- `[:phoenix_kit, :admin_sidebar, :render, :stop]` — emitted when rendering completes (includes `tab_count` in metadata)
```elixir
:telemetry.attach("admin-sidebar-monitor",
[:phoenix_kit, :admin_sidebar, :render, :stop],
fn _event, measurements, metadata, _config ->
Logger.debug("Admin sidebar rendered #{metadata.tab_count} tabs in #{measurements.duration}ns")
end,
nil
)
```