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
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ spark_locals_without_parens = [
format_fields: 1,
generic_actions: 1,
label_field: 1,
link_relationships?: 1,
max_file_size: 1,
name: 1,
polymorphic_actions: 1,
Expand Down
1 change: 1 addition & 0 deletions documentation/dsls/DSL-AshAdmin.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Configure the admin dashboard for a given resource.
| [`show_relationship_fields`](#admin-show_relationship_fields){: #admin-show_relationship_fields } | `keyword` | | The fields to render for each relationship on this resource's show page, keyed by relationship name. For example: `show_relationship_fields organizations: [:name, :slug]`. Fields are shown in the given order. Relationships that are not listed show every attribute. |
| [`label_field`](#admin-label_field){: #admin-label_field } | `atom` | | The field to use as the label when the resource appears in a relationship select or typeahead field on another resource's form. |
| [`relationship_select_max_items`](#admin-relationship_select_max_items){: #admin-relationship_select_max_items } | `integer` | `50` | The maximum number of items to show in a select field when this resource is shown as a relationship on another resource's form. If the number of related resources is higher, a typeahead selector will be used. |
| [`link_relationships?`](#admin-link_relationships?){: #admin-link_relationships? } | `boolean` | `true` | Whether `belongs_to` foreign key attributes (e.g. `user_id`) are rendered as links to the related record's show page. |


### admin.form
Expand Down
15 changes: 15 additions & 0 deletions lib/ash_admin/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -667,4 +667,19 @@ defmodule AshAdmin.CoreComponents do
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end

@doc """
Wraps `content` in a navigation link to `path`, or returns `content` as is when `path` is nil.
"""
def maybe_link(content, nil), do: content

def maybe_link(content, path) do
assigns = %{content: content, path: path}

~H"""
<.link navigate={@path} class="underline hover:text-slate-700 dark:hover:text-slate-300">
{@content}
</.link>
"""
end
end
24 changes: 22 additions & 2 deletions lib/ash_admin/components/resource/data_table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule AshAdmin.Components.Resource.DataTable do
attr :prefix, :any, required: true
attr :tenant, :any, required: true
attr :polymorphic_actions, :any, required: true
attr :domains, :list, default: nil

def render(assigns) do
~H"""
Expand Down Expand Up @@ -281,7 +282,21 @@ defmodule AshAdmin.Components.Resource.DataTable do
if assigns[:initialized] do
{:ok, socket}
else
socket = assign(socket, assigns)
socket =
socket
|> assign(assigns)
|> then(fn socket ->
assign(
socket,
:foreign_key_targets,
foreign_key_targets(
socket.assigns.resource,
socket.assigns.domain,
socket.assigns[:domains]
)
)
end)

params = socket.assigns[:params] || %{}
arguments = params["args"]

Expand Down Expand Up @@ -540,7 +555,12 @@ defmodule AshAdmin.Components.Resource.DataTable do
format_fields,
show_sensitive_fields,
[actor: assigns.actor, tenant: assigns[:tenant], authorize?: assigns[:authorizing]],
nil
nil,
foreign_key_path(
assigns[:prefix],
assigns[:foreign_key_targets][attribute.name],
Map.get(record, attribute.name)
)
)
else
"..."
Expand Down
3 changes: 3 additions & 0 deletions lib/ash_admin/components/resource/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ defmodule AshAdmin.Components.Resource do
attr :table, :any, default: nil
attr :tables, :any, default: nil
attr :prefix, :any, default: nil
attr :domains, :list, default: nil
attr :action_type, :atom
attr :polymorphic_actions, :any

Expand Down Expand Up @@ -98,6 +99,7 @@ defmodule AshAdmin.Components.Resource do
tenant={@tenant}
table={@table}
prefix={@prefix}
domains={@domains}
/>
<.live_component
:if={@action_type == :create}
Expand Down Expand Up @@ -132,6 +134,7 @@ defmodule AshAdmin.Components.Resource do
table={@table}
tables={@tables}
prefix={@prefix}
domains={@domains}
tenant={@tenant}
/>
<.live_component
Expand Down
48 changes: 25 additions & 23 deletions lib/ash_admin/components/resource/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ defmodule AshAdmin.Components.Resource.Show do
attr :tenant, :any
attr :table, :any, required: true
attr :prefix, :any, required: true
attr :domains, :list, default: nil

def render(assigns) do
~H"""
Expand Down Expand Up @@ -239,13 +240,11 @@ defmodule AshAdmin.Components.Resource.Show do
|> then(&{:ok, &1})
end

defp render_relationship_data(assigns, record, %{
cardinality: :one,
name: name,
destination: destination,
context: context,
domain: destination_domain
}) do
defp render_relationship_data(
assigns,
record,
%{cardinality: :one, name: name, destination: destination} = relationship
) do
case Map.get(record, name) do
nil ->
"None"
Expand All @@ -256,8 +255,7 @@ defmodule AshAdmin.Components.Resource.Show do
record: record,
name: name,
destination: destination,
context: context,
destination_domain: destination_domain,
target: relationship_target(relationship, assigns[:domain], assigns[:domains]),
fields: AshAdmin.Resource.show_relationship_fields(assigns.resource, name)
)

Expand All @@ -266,19 +264,8 @@ defmodule AshAdmin.Components.Resource.Show do
{render_attributes(assigns, @record, @destination, @name, @fields)}
<div class="px-4 py-3 text-right sm:px-6">
<.link
:if={AshAdmin.Resource.show_action(@destination)}
navigate={
admin_path(@prefix,
domain:
AshAdmin.Domain.name(
@destination_domain || Ash.Resource.Info.domain(@destination) || @domain
),
resource: AshAdmin.Resource.name(@destination),
table: @context[:data_layer][:table],
primary_key: encode_primary_key(@record),
action_type: "read"
)
}
:if={@target}
navigate={record_path(@prefix, @target, @record)}
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-slate-800 hover:bg-slate-700 dark:bg-slate-200 dark:hover:bg-slate-300 dark:text-slate-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500"
>
Show
Expand Down Expand Up @@ -321,6 +308,7 @@ defmodule AshAdmin.Components.Resource.Show do
domain={@destination_domain || Ash.Resource.Info.domain(@destination) || @domain}
table={@context[:data_layer][:table]}
prefix={@prefix}
domains={@domains}
skip={[@destination_attribute]}
relationship_name={@relationship_name}
actor={@actor}
Expand Down Expand Up @@ -349,7 +337,8 @@ defmodule AshAdmin.Components.Resource.Show do
attributes: attributes,
flags: flags,
bottom_attributes: bottom_attributes,
relationship_name: relationship_name
relationship_name: relationship_name,
foreign_key_targets: foreign_key_targets(resource, assigns[:domain], assigns[:domains])
)

~H"""
Expand Down Expand Up @@ -722,7 +711,20 @@ defmodule AshAdmin.Components.Resource.Show do
nested: nested?
)

link_path =
foreign_key_path(
assigns[:prefix],
assigns[:foreign_key_targets][attribute.name],
Map.get(record, attribute.name)
)

case attribute.type do
_ when not is_nil(link_path) ->
resource
|> AshAdmin.Resource.format_fields()
|> FormatHelper.format_attribute(record, attribute)
|> maybe_link(link_path)

Ash.Type.String ->
cond do
short_text?(resource, attribute) ->
Expand Down
34 changes: 27 additions & 7 deletions lib/ash_admin/components/resource/table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ defmodule AshAdmin.Components.Resource.Table do
attr :tenant, :any, default: nil
attr :authorizing, :boolean, default: false
attr :relationship_name, :atom, default: nil
attr :domains, :list, default: nil, doc: "The domains the admin shows, to link foreign keys"

def table(assigns) do
assigns =
assign(
assigns,
:foreign_key_targets,
foreign_key_targets(assigns.resource, assigns.domain, assigns.domains)
)

~H"""
<div>
<table class="rounded-t-lg m-5 w-5/6 mx-auto text-left">
Expand All @@ -50,7 +58,12 @@ defmodule AshAdmin.Components.Resource.Table do
@format_fields,
@show_sensitive_fields,
[actor: @actor, tenant: @tenant, authorize?: @authorizing],
@relationship_name
@relationship_name,
foreign_key_path(
@prefix,
@foreign_key_targets[attribute.name],
Map.get(record, attribute.name)
)
)}
</td>
<td :if={@actions && actions?(@resource)}>
Expand Down Expand Up @@ -146,7 +159,8 @@ defmodule AshAdmin.Components.Resource.Table do
formats,
show_sensitive_fields,
load_opts,
relationship_name
relationship_name,
link_path \\ nil
) do
process_attribute(
domain,
Expand All @@ -155,7 +169,8 @@ defmodule AshAdmin.Components.Resource.Table do
formats,
show_sensitive_fields,
load_opts,
relationship_name
relationship_name,
link_path
)
rescue
_ ->
Expand All @@ -169,7 +184,8 @@ defmodule AshAdmin.Components.Resource.Table do
formats,
show_sensitive_fields,
load_opts,
relationship_name
relationship_name,
_link_path
)
when module in [HasOne, BelongsTo] do
display_attributes = AshAdmin.Resource.relationship_display_fields(attribute.destination)
Expand Down Expand Up @@ -217,7 +233,8 @@ defmodule AshAdmin.Components.Resource.Table do
formats,
show_sensitive_fields,
_load_opts,
relationship_name
relationship_name,
link_path
)
when struct in [Ash.Resource.Attribute, Ash.Resource.Aggregate, Ash.Resource.Calculation] do
data = FormatHelper.format_attribute(formats, record, attribute)
Expand All @@ -226,7 +243,9 @@ defmodule AshAdmin.Components.Resource.Table do
not Enum.member?(show_sensitive_fields, attribute.name) do
format_sensitive_value(data, attribute, record, relationship_name)
else
format_attribute_value(data, attribute)
data
|> format_attribute_value(attribute)
|> maybe_link(link_path)
end
end

Expand All @@ -237,7 +256,8 @@ defmodule AshAdmin.Components.Resource.Table do
_formats,
_show_sensitive_fields,
_load_opts,
_relationship_name
_relationship_name,
_link_path
) do
"..."
end
Expand Down
85 changes: 85 additions & 0 deletions lib/ash_admin/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,91 @@ defmodule AshAdmin.Helpers do
prefix <> "?" <> query
end

@doc """
Resolves where the admin shows the destination of `relationship`, as a map of
`:domain`, `:resource` and `:table`, or returns `nil` when the destination
can't be opened in this admin.

`domains` is the list of domains the admin shows (its `:ash_domains`). A link
to any other domain would silently open the first shown domain instead.
"""
def relationship_target(relationship, current_domain, domains) do
destination = relationship.destination
domain = relationship.domain || Ash.Resource.Info.domain(destination) || current_domain

if visible_resource?(domain, destination, domains) do
%{
domain: domain,
resource: destination,
table: relationship.context[:data_layer][:table]
}
end
end

@doc """
Returns the admin show path of `record` at a target from `relationship_target/3`.
"""
def record_path(prefix, %{domain: domain, resource: resource, table: table}, record) do
admin_path(prefix,
domain: AshAdmin.Domain.name(domain),
resource: AshAdmin.Resource.name(resource),
table: table,
primary_key: encode_primary_key(record),
action_type: "read"
)
end

@doc """
Returns a map of the `belongs_to` foreign key attributes of `resource` that
should link to the related record, to their target (see `relationship_target/3`).

Resolve it once per resource and look attributes up in it, instead of
resolving it for every rendered value.
"""
def foreign_key_targets(resource, current_domain, domains) do
if AshAdmin.Resource.link_relationships?(resource) do
resource
|> Ash.Resource.Info.relationships()
|> Enum.filter(fn relationship ->
relationship.type == :belongs_to and
[relationship.destination_attribute] ==
Ash.Resource.Info.primary_key(relationship.destination)
end)
|> Enum.flat_map(fn relationship ->
case relationship_target(relationship, current_domain, domains) do
nil -> []
target -> [{relationship.source_attribute, target}]
end
end)
|> Map.new()
else
%{}
end
end

@doc """
Returns the admin show path of the record a foreign key `value` references,
or `nil` when there is no target or the value isn't an actual key.
"""
def foreign_key_path(_prefix, nil, _value), do: nil
def foreign_key_path(nil, _target, _value), do: nil
def foreign_key_path(_prefix, _target, nil), do: nil
def foreign_key_path(_prefix, _target, %Ash.NotLoaded{}), do: nil
def foreign_key_path(_prefix, _target, %Ash.ForbiddenField{}), do: nil

def foreign_key_path(prefix, %{resource: resource} = target, value) do
[primary_key] = Ash.Resource.Info.primary_key(resource)
record_path(prefix, target, struct(resource, [{primary_key, value}]))
end

defp visible_resource?(domain, resource, domains) when is_list(domains) do
domain in domains and AshAdmin.Domain.show?(domain) and
resource in AshAdmin.Domain.show_resources(domain) and
not is_nil(AshAdmin.Resource.show_action(resource))
end

defp visible_resource?(_domain, _resource, _domains), do: false

def to_name(:id), do: "ID"

def to_name(%{__struct__: Ash.Resource.Attribute, related_resource: resource} = attribute)
Expand Down
1 change: 1 addition & 0 deletions lib/ash_admin/pages/page_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ defmodule AshAdmin.PageLive do
tables={@tables}
polymorphic_actions={@polymorphic_actions}
prefix={@prefix}
domains={@domains}
/>
</main>
</div>
Expand Down
Loading
Loading