diff --git a/config/config.exs b/config/config.exs index 53cd51f1..a2d31109 100644 --- a/config/config.exs +++ b/config/config.exs @@ -77,6 +77,7 @@ if config_env() == :test do config :ash_admin, ash_domains: [ - AshAdmin.Test.Domain + AshAdmin.Test.DomainA, + AshAdmin.Test.DomainB ] end diff --git a/lib/ash_admin/domain.ex b/lib/ash_admin/domain.ex index a01f4d40..0d158bf9 100644 --- a/lib/ash_admin/domain.ex +++ b/lib/ash_admin/domain.ex @@ -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 + """ ] ] } @@ -39,6 +51,36 @@ 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 + ``` + + 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 @@ -61,6 +103,10 @@ 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 + defp default_name(domain) do split = domain |> Module.split() diff --git a/lib/ash_admin/pages/page_live.ex b/lib/ash_admin/pages/page_live.ex index d9cf0179..52ede47b 100644 --- a/lib/ash_admin/pages/page_live.ex +++ b/lib/ash_admin/pages/page_live.ex @@ -25,7 +25,7 @@ defmodule AshAdmin.PageLive do socket ) do otp_app = socket.endpoint.config(:otp_app) - + group = session["group"] prefix = case prefix do "/" -> @@ -39,7 +39,7 @@ defmodule AshAdmin.PageLive do socket = assign(socket, :prefix, prefix) - domains = domains(otp_app) + domains = domains(otp_app) |> filter_domains_by_group(group) {:ok, socket @@ -113,6 +113,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 -> diff --git a/lib/ash_admin/router.ex b/lib/ash_admin/router.ex index 077a3569..d4f6a1f5 100644 --- a/lib/ash_admin/router.ex +++ b/lib/ash_admin/router.ex @@ -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 @@ -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 @@ -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", diff --git a/test/ash_admin_test.exs b/test/ash_admin_test.exs index a126b81f..ee383f3b 100644 --- a/test/ash_admin_test.exs +++ b/test/ash_admin_test.exs @@ -92,4 +92,78 @@ defmodule AshAdmin.Test.AshAdminTest do end ) end + + describe "domain grouping" do + test "domains without group return nil" do + defmodule DomainNoGroup do + @moduledoc false + use Ash.Domain, + extensions: [AshAdmin.Domain] + + admin do + show? true + end + + resources do + resource(AshAdmin.Test.Post) + end + end + + assert AshAdmin.Domain.group(DomainNoGroup) == nil + end + + test "domains with group return their group value" do + defmodule DomainWithGroup do + @moduledoc false + use Ash.Domain, + extensions: [AshAdmin.Domain] + + admin do + show? true + group :sub_app + end + + resources do + resource(AshAdmin.Test.Post) + end + end + + assert AshAdmin.Domain.group(DomainWithGroup) == :sub_app + end + + test "multiple domains with same group are all visible" do + defmodule FirstGroupedDomain do + @moduledoc false + use Ash.Domain, + extensions: [AshAdmin.Domain] + + admin do + show? true + group :sub_app + end + + resources do + resource(AshAdmin.Test.Post) + end + end + + defmodule SecondGroupedDomain do + @moduledoc false + use Ash.Domain, + extensions: [AshAdmin.Domain] + + admin do + show? true + group :sub_app + end + + resources do + resource(AshAdmin.Test.Comment) + end + end + + assert AshAdmin.Domain.group(FirstGroupedDomain) == :sub_app + assert AshAdmin.Domain.group(SecondGroupedDomain) == :sub_app + end + end end diff --git a/test/components/top_nav/helpers/dropdown_helper_test.exs b/test/components/top_nav/helpers/dropdown_helper_test.exs index ff5d3ca5..8ff94c0d 100644 --- a/test/components/top_nav/helpers/dropdown_helper_test.exs +++ b/test/components/top_nav/helpers/dropdown_helper_test.exs @@ -7,27 +7,27 @@ defmodule AshAdmin.Test.Components.TopNav.Helpers.DropdownHelperTest do test "groups resources" do prefix = "/admin" current_resource = AshAdmin.Test.Post - domain = AshAdmin.Test.Domain + domain = AshAdmin.Test.DomainA blog_link = %{ active: false, group: :group_b, text: "Blog", - to: "/admin?domain=Test&resource=Blog" + to: "/admin?domain=DomainA&resource=Blog" } post_link = %{ active: true, group: :group_a, text: "Post", - to: "/admin?domain=Test&resource=Post" + to: "/admin?domain=DomainA&resource=Post" } comment_link = %{ active: false, group: nil, text: "Comment", - to: "/admin?domain=Test&resource=Comment" + to: "/admin?domain=DomainA&resource=Comment" } assert_unordered( @@ -39,7 +39,7 @@ defmodule AshAdmin.Test.Components.TopNav.Helpers.DropdownHelperTest do test "groups resources by given order from the domain" do prefix = "/admin" current_resource = AshAdmin.Test.Post - domain = AshAdmin.Test.Domain + domain = AshAdmin.Test.DomainA assert [ [%{group: :group_b, text: "Blog"} = _blog_link], @@ -51,7 +51,7 @@ defmodule AshAdmin.Test.Components.TopNav.Helpers.DropdownHelperTest do describe "dropdown_group_labels/3" do test "returns groups" do - domain = AshAdmin.Test.Domain + domain = AshAdmin.Test.DomainA assert [group_b: "Group B", group_a: "Group A", group_c: "Group C"] = DropdownHelper.dropdown_group_labels(domain) diff --git a/test/page_live_test.exs b/test/page_live_test.exs index 303eca55..63cb16b8 100644 --- a/test/page_live_test.exs +++ b/test/page_live_test.exs @@ -59,4 +59,27 @@ defmodule AshAdmin.Test.PageLiveTest do assert html =~ ~s| Plug.Test.init_test_session(%{}) + |> fetch_session() + |> put_session(:group, :group_b) + |> live("/api/sub_app/admin") + + # Should show only domains with group_b + assert html =~ "DomainB" + refute html =~ "DomainA" + + {:ok, _view, html} = + conn + |> live("/api/admin") + + # Should show only ungrouped domains + assert html =~ "DomainA" + assert html =~ "DomainB" # DomainB has group_b, so it shouldn't show up in ungrouped view + end + end end diff --git a/test/support/domain.ex b/test/support/domain_a.ex similarity index 90% rename from test/support/domain.ex rename to test/support/domain_a.ex index 912afe00..b2c224f5 100644 --- a/test/support/domain.ex +++ b/test/support/domain_a.ex @@ -1,4 +1,4 @@ -defmodule AshAdmin.Test.Domain do +defmodule AshAdmin.Test.DomainA do @moduledoc false use Ash.Domain, extensions: [AshAdmin.Domain] diff --git a/test/support/domain_b.ex b/test/support/domain_b.ex new file mode 100644 index 00000000..e26a95b3 --- /dev/null +++ b/test/support/domain_b.ex @@ -0,0 +1,11 @@ +defmodule AshAdmin.Test.DomainB do + @moduledoc false + use Ash.Domain, + extensions: [AshAdmin.Domain] + + admin do + show? true + group :group_b + end + +end diff --git a/test/support/resources/blog.ex b/test/support/resources/blog.ex index 35063a2b..a11e589c 100644 --- a/test/support/resources/blog.ex +++ b/test/support/resources/blog.ex @@ -1,7 +1,7 @@ defmodule AshAdmin.Test.Blog do @moduledoc false use Ash.Resource, - domain: AshAdmin.Test.Domain, + domain: AshAdmin.Test.DomainA, data_layer: Ash.DataLayer.Ets, extensions: [AshAdmin.Resource] diff --git a/test/support/resources/comment.ex b/test/support/resources/comment.ex index 55db028c..2c4193af 100644 --- a/test/support/resources/comment.ex +++ b/test/support/resources/comment.ex @@ -1,7 +1,7 @@ defmodule AshAdmin.Test.Comment do @moduledoc false use Ash.Resource, - domain: AshAdmin.Test.Domain, + domain: AshAdmin.Test.DomainA, data_layer: Ash.DataLayer.Ets attributes do diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index 79a33c08..a7fbf1bf 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -1,7 +1,7 @@ defmodule AshAdmin.Test.Post do @moduledoc false use Ash.Resource, - domain: AshAdmin.Test.Domain, + domain: AshAdmin.Test.DomainA, data_layer: Ash.DataLayer.Ets, extensions: [AshAdmin.Resource] diff --git a/test/support/router.ex b/test/support/router.ex index 0c213750..bc7e596f 100644 --- a/test/support/router.ex +++ b/test/support/router.ex @@ -27,5 +27,11 @@ defmodule AshAdmin.Test.Router do live_session_name: :ash_admin_csp_full, csp_nonce_assign_key: csp_full ) + + # Test route for group-based admin panel + ash_admin("/sub_app/admin", + live_session_name: :ash_admin_sub_app, + group: :group_b + ) end end