Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ if config_env() == :test do

config :ash_admin,
ash_domains: [
AshAdmin.Test.Domain
AshAdmin.Test.DomainA,
AshAdmin.Test.DomainB
]
end
8 changes: 7 additions & 1 deletion lib/ash_admin/components/resource/info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ defmodule AshAdmin.Components.Resource.Info do
attr :resource, :any, required: true
attr :domain, :any, required: true
attr :prefix, :any, required: true
attr :current_group, :any, default: nil

def info(assigns) do
~H"""
<div class="relative mx-12">
<MetadataTable.attribute_table resource={@resource} />
<MetadataTable.relationship_table domain={@domain} resource={@resource} prefix={@prefix} />
<MetadataTable.relationship_table
domain={@domain}
resource={@resource}
prefix={@prefix}
current_group={@current_group}
/>
</div>
"""
end
Expand Down
26 changes: 23 additions & 3 deletions lib/ash_admin/components/resource/metadata_table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ defmodule AshAdmin.Components.Resource.MetadataTable do
attr :resource, :any, required: true
attr :domain, :any, required: true
attr :prefix, :any, required: true
attr :current_group, :any, default: nil

def relationship_table(assigns) do
~H"""
Expand Down Expand Up @@ -98,9 +99,16 @@ defmodule AshAdmin.Components.Resource.MetadataTable do
{relationship.type}
</.td>
<.td>
<.link navigate={"#{@prefix}?domain=#{AshAdmin.Domain.name(@domain)}&resource=#{AshAdmin.Resource.name(relationship.destination)}"}>
{AshAdmin.Resource.name(relationship.destination)}
</.link>
<%= %>
<%= if destination_domain_accessible?(relationship, @domain, @current_group) do %>
<.link navigate={"#{@prefix}?domain=#{AshAdmin.Domain.name(destination_domain(relationship, @domain))}&resource=#{AshAdmin.Resource.name(relationship.destination)}"}>
{AshAdmin.Resource.name(relationship.destination)}
</.link>
<% else %>
<span class="text-gray-400">
{AshAdmin.Resource.name(relationship.destination)} (not accessible)
</span>
<% end %>
</.td>
<.td class="max-w-sm min-w-sm text-gray-500">
{relationship.description}
Expand Down Expand Up @@ -175,4 +183,16 @@ defmodule AshAdmin.Components.Resource.MetadataTable do
|> Ash.Resource.Info.relationships()
|> Enum.sort_by(&(not &1.public?))
end

defp destination_domain(relationship, fallback \\ nil) do
Ash.Resource.Info.domain(relationship.destination) || fallback
end

defp destination_domain_accessible?(relationship, domain, current_group) do
# Get the destination domain of the relationship
destination_domain = destination_domain(relationship, domain)

# Use the helper function from AshAdmin.Domain to check accessibility
AshAdmin.Domain.domain_accessible_in_group?(destination_domain, current_group)
end
end
10 changes: 9 additions & 1 deletion lib/ash_admin/components/resource/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ defmodule AshAdmin.Components.Resource do
attr :prefix, :any, default: nil
attr :action_type, :atom
attr :polymorphic_actions, :any
attr :current_group, :any, default: nil

def render(assigns) do
~H"""
Expand Down Expand Up @@ -90,8 +91,15 @@ defmodule AshAdmin.Components.Resource do
tenant={@tenant}
table={@table}
prefix={@prefix}
current_group={@current_group}
/>
<Info.info
:if={is_nil(@action_type)}
resource={@resource}
domain={@domain}
prefix={@prefix}
current_group={@current_group}
/>
<Info.info :if={is_nil(@action_type)} resource={@resource} domain={@domain} prefix={@prefix} />
<.live_component
:if={@action_type == :create}
module={Form}
Expand Down
23 changes: 19 additions & 4 deletions lib/ash_admin/components/resource/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule AshAdmin.Components.Resource.Show do
attr :tenant, :any
attr :table, :any, required: true
attr :prefix, :any, required: true
attr :current_group, :any, default: nil

def render(assigns) do
~H"""
Expand Down Expand Up @@ -142,11 +143,23 @@ defmodule AshAdmin.Components.Resource.Show do
end

defp render_relationships(assigns, _record, resource) do
assigns = assign(assigns, resource: resource)
# Filter relationships to only include those with accessible domains
accessible_relationships =
resource
|> AshAdmin.Components.Resource.Form.relationships(:show)
|> Enum.filter(fn relationship ->
AshAdmin.Domain.domain_accessible_in_group?(
Ash.Resource.Info.domain(relationship.destination),
assigns[:current_group]
)
end)

assigns =
assign(assigns, resource: resource, accessible_relationships: accessible_relationships)

~H"""
<div
:for={relationship <- AshAdmin.Components.Resource.Form.relationships(@resource, :show)}
:for={relationship <- @accessible_relationships}
class="shadow-lg overflow-hidden sm:rounded-md mb-2 bg-white"
>
<div class="px-4 py-5 mt-2">
Expand Down Expand Up @@ -221,9 +234,10 @@ defmodule AshAdmin.Components.Resource.Show do
cardinality: :one,
name: name,
destination: destination,
context: context,
domain: destination_domain
context: context
}) do
destination_domain = Ash.Resource.Info.domain(destination)

case Map.get(record, name) do
nil ->
"None"
Expand Down Expand Up @@ -283,6 +297,7 @@ defmodule AshAdmin.Components.Resource.Show do
prefix={@prefix}
skip={[@destination_attribute]}
relationship_name={@relationship_name}
current_group={@current_group}
/>
</div>
"""
Expand Down
1 change: 1 addition & 0 deletions lib/ash_admin/components/resource/table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule AshAdmin.Components.Resource.Table do
attr :show_sensitive_fields, :list, default: []
attr :actor, :any, default: nil
attr :relationship_name, :atom, default: nil
attr :current_group, :any, default: nil

def table(assigns) do
~H"""
Expand Down
78 changes: 78 additions & 0 deletions lib/ash_admin/domain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ defmodule AshAdmin.Domain do
default: [],
doc:
"Humanized names for each resource group to appear in the admin area. These will be used as labels in the top navigation dropdown and will be shown sorted as given. If a key for a group does not appear in this mapping, the label will not be rendered."
],
group: [
type: :atom,
default: nil,
doc: """
The group for filtering multiple admin dashboards. When set, this domain will only appear
in admin routes that specify a matching group option. If not set (nil), the domain will
only appear in admin routes without group filtering.

Example:
group :sub_app # This domain will only show up in routes with group: :sub_app
"""
]
]
}
Expand All @@ -39,6 +51,43 @@ defmodule AshAdmin.Domain do

@moduledoc """
A domain extension to alter the behavior of a domain in the admin UI.

## Group-based Filtering

Domains can be assigned to groups using the `group` option in the admin configuration.
This allows you to create multiple admin dashboards, each showing only the domains that belong
to a specific group.

### Example

```elixir
defmodule MyApp.SomeFeatureDomain do
use Ash.Domain,
extensions: [AshAdmin.Domain]

admin do
show? true
group :sub_app # This domain will only appear in admin routes with group: :sub_app
end

# ... rest of domain configuration
end
```

Then in your router:
```elixir
ash_admin "/sub_app/admin", group: :sub_app # Will only show domains with group: :sub_app
```

You might need to define different `live_session_name` for the admin dashboards in your
router, depending on the group. For example:

```elixir
ash_admin "/sub_app/admin", group: :sub_app, live_session_name: :sub_app_admin
```

Note: If you add a group filter to your admin route but haven't set the corresponding group
in your domains' admin configuration, those domains won't appear in the admin interface.
"""

def name(domain) do
Expand All @@ -61,6 +110,35 @@ defmodule AshAdmin.Domain do
Spark.Dsl.Extension.get_opt(domain, [:admin], :resource_group_labels, [], true)
end

def group(domain) do
Spark.Dsl.Extension.get_opt(domain, [:admin], :group, nil, true)
end

@doc """
Checks if a destination domain is accessible from the current group context.

Returns true if:
- No group filtering is active (current_group is nil)
- The destination domain belongs to the same group as current_group
- The destination domain has no group (nil) and current_group is also nil
"""
def domain_accessible_in_group?(destination_domain, current_group) do
destination_group = group(destination_domain)

case {current_group, destination_group} do
# No group filtering, ungrouped domain
{nil, nil} -> true
# No group filtering, but domain has a group
{nil, _} -> false
# Same group
{group, group} -> true
# Group filtering active, but domain has no group
{_, nil} -> false
# Different groups
{_, _} -> false
end
end

defp default_name(domain) do
split = domain |> Module.split()

Expand Down
13 changes: 11 additions & 2 deletions lib/ash_admin/pages/page_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ defmodule AshAdmin.PageLive do
socket
) do
otp_app = socket.endpoint.config(:otp_app)

group = session["group"]
prefix =
case prefix do
"/" ->
Expand All @@ -39,14 +39,15 @@ defmodule AshAdmin.PageLive do

socket = assign(socket, :prefix, prefix)

domains = domains(otp_app)
domains = domains(otp_app) |> filter_domains_by_group(group)

{:ok,
socket
|> assign(:prefix, prefix)
|> assign(:primary_key, nil)
|> assign(:record, nil)
|> assign(:domains, domains)
|> assign(:current_group, group)
|> assign(:tenant, session["tenant"])
|> assign(:editing_tenant, false)
|> then(fn socket ->
Expand Down Expand Up @@ -103,6 +104,7 @@ defmodule AshAdmin.PageLive do
tables={@tables}
polymorphic_actions={@polymorphic_actions}
prefix={@prefix}
current_group={@current_group}
/>
"""
end
Expand All @@ -113,6 +115,13 @@ defmodule AshAdmin.PageLive do
|> Enum.filter(&AshAdmin.Domain.show?/1)
end

defp filter_domains_by_group(domains, nil), do: domains
defp filter_domains_by_group(domains, group) do
Enum.filter(domains, fn domain ->
AshAdmin.Domain.group(domain) == group
end)
end

defp assign_domain(socket, domain) do
domain =
Enum.find(socket.assigns.domains, fn shown_domain ->
Expand Down
24 changes: 22 additions & 2 deletions lib/ash_admin/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ defmodule AshAdmin.Router do

* `:live_session_name` - Optional atom to name the `live_session`. Defaults to `:ash_admin`.

* `:group` - Optional atom to filter domains by group. Only domains with a matching group will be shown.
For example: `group: :sub_app` will only show domains with `group: :sub_app` in their admin configuration.
Note: If you specify a group here but haven't set that group in any domain's admin configuration,
the admin interface will appear empty. Make sure to configure the group in your domains:
```elixir
# In your domain:
admin do
show? true
group :sub_app
end
```

## Examples
defmodule MyAppWeb.Router do
use Phoenix.Router
Expand All @@ -71,7 +83,9 @@ defmodule AshAdmin.Router do
# If you don't have one, see `admin_browser_pipeline/1`
pipe_through [:browser]

ash_admin "/admin"
# Default route - shows all domains that don't have a group set
ash_admin "/admin" # Shows all domains with no group filter
ash_admin "/sub_app/admin", group: :sub_app # Only shows domains with group: :sub_app
ash_admin "/csp/admin", live_session_name: :ash_admin_csp, csp_nonce_assign_key: :csp_nonce_value
end
end
Expand Down Expand Up @@ -100,7 +114,13 @@ defmodule AshAdmin.Router do
live_session opts[:live_session_name] || :ash_admin,
on_mount: List.wrap(opts[:on_mount]),
session:
{AshAdmin.Router, :__session__, [%{"prefix" => path}, List.wrap(opts[:session])]},
{AshAdmin.Router, :__session__, [
Map.merge(
%{"prefix" => path},
if(opts[:group], do: %{"group" => opts[:group]}, else: %{})
),
List.wrap(opts[:session])
]},
root_layout: {AshAdmin.Layouts, :root} do
live(
"#{path}/*route",
Expand Down
Loading