defmodule AshAdmin.Resource do @field %Spark.Dsl.Entity{ describe: "Declare non-default behavior for a specific attribute.", name: :field, schema: AshAdmin.Resource.Field.schema(), target: AshAdmin.Resource.Field, args: [:name] } @form %Spark.Dsl.Section{ describe: "Configure the appearance of fields in admin forms.", name: :form, entities: [ @field ] } @admin %Spark.Dsl.Section{ describe: "Configure the admin dashboard for a given resource.", name: :admin, sections: [ @form ], schema: [ name: [ type: :string, doc: "The proper name to use when this resource appears in the admin interface." ], actor?: [ type: :boolean, doc: "Whether or not this resource can be used as the actor for requests." ], show_action: [ type: :atom, doc: "The action to use when linking to the resource/viewing a single record. Defaults to the primary read action." ], read_actions: [ type: {:list, :atom}, doc: "A list of read actions that can be used to show resource details. By default, all actions are included." ], generic_actions: [ type: {:list, :atom}, doc: "A list of generic actions that can be used to show resource details. By default, all actions are included." ], create_actions: [ type: {:list, :atom}, doc: "A list of create actions that can create records. By default, all actions are included." ], update_actions: [ type: {:list, :atom}, doc: "A list of update actions that can be used to update records. By default, all actions are included." ], destroy_actions: [ type: {:list, :atom}, doc: "A list of destroy actions that can be used to destroy records. By default, all actions are included." ], polymorphic_tables: [ type: {:list, :string}, doc: """ For resources that use ash_postgres' polymorphism capabilities, you can provide a list of tables that should be available to select. These will be added to the list of derivable tables based on scanning all domains and resources provided to ash_admin. """ ], polymorphic_actions: [ type: {:list, :atom}, doc: """ For resources that use ash_postgres' polymorphism capabilities, you can provide a list of actions that should require a table to be set. If this is not set, then *all* actions will require tables. """ ], table_columns: [ type: {:list, :atom}, doc: "The list of attributes to render on the table view." ], format_fields: [ type: {:list, :any}, doc: """ The list of fields and their formats represented as a MFA. For example: `updated_at: {Timex, :format!, ["{0D}-{0M}-{YYYY} {h12}:{m} {AM}"]}`. Datatable pages format all given fields. Show and Update pages format given read-only fields of types `Ash.Type.Date`, `Ash.Type.DateTime`, `Ash.Type.Time`, `Ash.Type.NaiveDatetime`, `Ash.Type.UtcDatetime` and `Ash.Type.UtcDatetimeUsec`. """ ], relationship_display_fields: [ type: {:list, :atom}, doc: "The list of attributes to render when this resource is shown as a relationship on another resource's datatable." ], resource_group: [ type: :atom, doc: "The group in the top resource dropdown that the resource appears in." ], show_sensitive_fields: [ type: {:list, :atom}, doc: "The list of fields that should not be redacted in the admin UI even if they are marked as sensitive." ], show_calculations: [ type: {:list, :atom}, doc: "A list of calculation that can be calculate when this resource is shown. By default, all calculations are included." ] ] } use Spark.Dsl.Extension, sections: [@admin], transformers: [AshAdmin.Resource.Transformers.ValidateTableColumns] @moduledoc """ A resource extension to alter the behaviour of a resource in the admin UI. """ def polymorphic?(resource, domains) do polymorphic_tables(resource, domains) not in [nil, []] end def polymorphic_tables(resource, domains) do resource |> Spark.Dsl.Extension.get_opt([:admin], :polymorphic_tables, [], true) |> Enum.concat(find_polymorphic_tables(resource, domains)) |> Enum.uniq() end def polymorphic_actions(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :polymorphic_actions, nil, true) end def relationship_display_fields(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :relationship_display_fields, nil, true) end def table_columns(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :table_columns, nil, true) || Enum.map(Ash.Resource.Info.attributes(resource), & &1.name) end def format_fields(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :format_fields, nil, true) end def name(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :name, nil, true) || resource |> Module.split() |> List.last() end def resource_group(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :resource_group, nil, true) end def show_sensitive_fields(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :show_sensitive_fields, [], true) end def actor?(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :actor?, false, true) end def read_actions(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :read_actions, nil, true) || actions_with_primary_first(resource, :read) end def generic_actions(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :generic_actions, nil, true) || actions_with_primary_first(resource, :action) end def create_actions(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :create_actions, nil, true) || actions_with_primary_first(resource, :create) end def update_actions(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :update_actions, nil, true) || actions_with_primary_first(resource, :update) end def destroy_actions(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :destroy_actions, nil, true) || actions_with_primary_first(resource, :destroy) end def show_action(resource) do action = Spark.Dsl.Extension.get_opt(resource, [:admin], :show_action, false, []) if action do action else action = AshAdmin.Helpers.primary_action(resource, :read) action && action.name end end def show_calculations(resource) do Spark.Dsl.Extension.get_opt(resource, [:admin], :show_calculations, [], true) end def fields(resource) do Spark.Dsl.Extension.get_entities(resource, [:admin, :form]) end def field(resource, name) do resource |> fields() |> Enum.find(fn field -> field.name == name end) end defp find_polymorphic_tables(resource, domains) do domains |> Enum.flat_map(&AshAdmin.Domain.show_resources/1) |> Enum.flat_map(&Ash.Resource.Info.relationships/1) |> Enum.filter(&(&1.destination == resource)) |> Enum.map(& &1.context[:data_layer][:table]) |> Enum.reject(&is_nil/1) |> Enum.uniq() end defp actions_with_primary_first(resource, type) do resource |> Ash.Resource.Info.actions() |> Enum.filter(&(&1.type == type)) |> Enum.sort_by(&(!Map.get(&1, :primary?))) |> Enum.map(& &1.name) end end