Skip to content

Commit

Permalink
feat(Calculations): Able to show calculations on show page (#235)
Browse files Browse the repository at this point in the history
Co-authored-by: Zach Daniel <[email protected]>
  • Loading branch information
esdrasedu and zachdaniel authored Dec 11, 2024
1 parent 56a731f commit e563097
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 10 deletions.
30 changes: 30 additions & 0 deletions dev/resources/accounts/calculations/concat.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Demo.Calculations.Concat do
use Ash.Resource.Calculation

@impl true
def init(opts) do
if opts[:keys] && is_list(opts[:keys]) && Enum.all?(opts[:keys], &is_atom/1) do
{:ok, opts}
else
{:error, "Expected a `keys` option for which keys to concat"}
end
end

@impl true
def load(_query, opts, _context) do
opts[:keys]
end

@impl true
def calculate(records, opts, %{arguments: %{separator: separator}}) when is_bitstring(separator) do
Enum.map(records, fn record ->
Enum.map_join(opts[:keys], separator, fn key ->
to_string(Map.get(record, key))
end)
end)
end

def calculate(_records, _opts, _context) do
{:error, "Argument separator invalid"}
end
end
25 changes: 24 additions & 1 deletion dev/resources/accounts/resources/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ defmodule Demo.Accounts.User do
read_actions [:me, :read, :by_id, :by_name]

table_columns [:id, :first_name, :last_name, :representative, :admin, :full_name, :api_key, :date_of_birth]

show_calculations [:multi_arguments, :full_name]
end

multitenancy do
Expand Down Expand Up @@ -82,7 +84,28 @@ defmodule Demo.Accounts.User do
end

calculations do
calculate :full_name, :string, expr(first_name <> " " <> last_name)
calculate :full_name, :string, {Demo.Calculations.Concat, keys: [:first_name, :last_name]} do
argument :separator, :string do
allow_nil? false
constraints [allow_empty?: true, trim?: false]
default " "
end
end

calculate :is_super_admin?, :boolean, expr(admin && representative)

calculate :multi_arguments, :string, expr("Arg1: " <> ^arg(:arg1) <> ", Arg2: " <> (if ^arg(:arg2), do: "Yes", else: "No") <> ", Arg3: " <> ^arg(:arg3)) do
argument :arg1, :string do
allow_nil? false
constraints [allow_empty?: false]
end

argument :arg2, :boolean

argument :arg3, :float do
allow_nil? true
end
end
end

attributes do
Expand Down
27 changes: 20 additions & 7 deletions lib/ash_admin/components/resource/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ defmodule AshAdmin.Components.Resource.Form do
prompt={allow_nil_option(@attribute, @value)}
name={@name || @form.name <> "[#{@attribute.name}]"}
/>
<% markdown?(@form.source.resource, @attribute) -> %>
<% markdown?(@resource, @attribute) -> %>
<div
phx-hook="MarkdownEditor"
id={if @id, do: @id <> "_container", else: @form.id <> "_#{@attribute.name}_container"}
Expand All @@ -773,7 +773,7 @@ defmodule AshAdmin.Components.Resource.Form do
name={@name || @form.name <> "[#{@attribute.name}]"}
><%= value(@value, @form, @attribute) || "" %></textarea>
</div>
<% long_text?(@form.source.resource, @attribute) -> %>
<% long_text?(@resource, @attribute) -> %>
<textarea
id={@id || @form.id <> "_#{@attribute.name}"}
name={@name || @form.name <> "[#{@attribute.name}]"}
Expand All @@ -782,9 +782,9 @@ defmodule AshAdmin.Components.Resource.Form do
data-attrs="style"
placeholder={placeholder(@default)}
><%= value(@value, @form, @attribute) %></textarea>
<% short_text?(@form.source.resource, @attribute) -> %>
<% short_text?(@resource, @attribute) -> %>
<.input
type={text_input_type(@form.source.resource, @attribute)}
type={text_input_type(@resource, @attribute)}
id={@id || @form.id <> "_#{@attribute.name}"}
value={value(@value, @form, @attribute)}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
Expand All @@ -793,7 +793,7 @@ defmodule AshAdmin.Components.Resource.Form do
/>
<% true -> %>
<.input
type={text_input_type(@form.source.resource, @attribute)}
type={text_input_type(@resource, @attribute)}
placeholder={placeholder(@default)}
id={@id || @form.id <> "_#{@attribute.name}"}
value={value(@value, @form, @attribute)}
Expand Down Expand Up @@ -1374,8 +1374,17 @@ defmodule AshAdmin.Components.Resource.Form do
defp value(%Ash.Union{value: value}, _form, _attribute, _) when not is_nil(value), do: value
defp value(value, _form, _attribute, _) when not is_nil(value), do: value

defp value(_value, form, attribute, _default) do
case AshPhoenix.Form.value(form.source, attribute.name) do
defp value(
_value,
%{source: %AshPhoenix.FilterForm.Arguments{input: input}},
%{name: attribute_name},
_default
) do
Map.get(input, attribute_name, nil)
end

defp value(_value, %{source: form}, attribute, _default) do
case AshPhoenix.Form.value(form, attribute.name) do
%Ash.Union{value: value} -> value
value -> value
end
Expand Down Expand Up @@ -1835,6 +1844,10 @@ defmodule AshAdmin.Components.Resource.Form do

def attributes(resource, action, exactly \\ nil)

def attributes(resource, %Ash.Resource.Calculation{arguments: arguments}, _exacly) do
sort_attributes(arguments, resource)
end

def attributes(resource, %{type: :read, arguments: arguments}, exactly)
when not is_nil(exactly) do
resource
Expand Down
133 changes: 131 additions & 2 deletions lib/ash_admin/components/resource/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,40 @@ defmodule AshAdmin.Components.Resource.Show do
attr :prefix, :any, required: true

def render(assigns) do
assigns =
assign_new(assigns, :calculations, fn %{resource: resource} ->
calculations =
AshAdmin.Resource.show_calculations(resource)

resource
|> Ash.Resource.Info.calculations()
|> Enum.filter(&(&1.name in calculations))
|> Enum.sort_by(
&Enum.find_index(calculations, fn name ->
name == &1.name
end)
)
|> Enum.map(fn calculation ->
form =
AshPhoenix.FilterForm.Arguments.new(%{}, calculation.arguments)
|> to_form()

{calculation, form}
end)
end)

~H"""
<div class="md:pt-10 sm:mt-0 bg-gray-300 min-h-screen pb-20">
<div class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:mt-10">
<div class="mt-5 md:mt-0 md:col-span-2">
{render_show(assigns, @record, @resource)}
</div>
</div>
<div class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:mt-10">
<div class="mt-5 md:mt-0 md:col-span-2">
{render_calculations(assigns, @record, @resource)}
</div>
</div>
<div class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:mt-10">
<div class="mt-5 md:mt-0 md:col-span-2">
{render_relationships(assigns, @record, @resource)}
Expand Down Expand Up @@ -75,6 +102,65 @@ defmodule AshAdmin.Components.Resource.Show do
"""
end

@spec render_calculations(any(), any(), any()) :: Phoenix.LiveView.Rendered.t()
def render_calculations(assigns, record, resource) do
assigns = assign(assigns, record: record, resource: resource)

~H"""

Check warning on line 109 in lib/ash_admin/components/resource/show.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

guard_fail

The guard clause can never succeed.
<div
:for={{calculation, calculation_form} <- @calculations}
class="shadow-lg overflow-hidden sm:rounded-md mb-2 bg-white"
>
<div class="px-4 py-5 mt-2">
<div>{to_name(calculation.name)}</div>
<div :if={loaded?(@record, calculation.name)}>
{render_maybe_sensitive_attribute(
assigns,
@resource,
@record,
calculation
)}
</div>
<div>
<.form
:let={form}
:if={length(calculation.arguments)}
as={calculation.name}
for={calculation_form}
phx-submit="calculate"
phx-target={@myself}
>
<.input type="hidden" name="calculation" value={calculation.name} />
{AshAdmin.Components.Resource.Form.render_attributes(
assigns,
@resource,
calculation,
form
)}
<.error :if={is_exception(@calculation_errors[calculation.name])}>
{Exception.message(@calculation_errors[calculation.name])}
</.error>
<.error :if={
@calculation_errors[calculation.name] &&
!is_exception(@calculation_errors[calculation.name])
}>
{inspect(@calculation_errors[calculation.name])}
</.error>
<div class="px-4 py-3 text-right sm:px-6 text-right">
<button
type="submit"
class="py-2 px-4 mt-2 bg-indigo-600 text-white border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
Calculate
</button>
</div>
</.form>
</div>
</div>
</div>
"""
end

defp render_relationships(assigns, _record, resource) do
assigns = assign(assigns, resource: resource)

Expand Down Expand Up @@ -117,7 +203,12 @@ defmodule AshAdmin.Components.Resource.Show do
end

def mount(socket) do
{:ok, assign_new(socket, :load_errors, fn -> %{} end)}
assign =
socket
|> assign_new(:load_errors, fn -> %{} end)
|> assign_new(:calculation_errors, fn -> %{} end)

{:ok, assign}
end

defp render_relationship_data(assigns, record, %{
Expand Down Expand Up @@ -289,7 +380,13 @@ defmodule AshAdmin.Components.Resource.Show do
"""
end

defp render_maybe_sensitive_attribute(assigns, resource, record, attribute, relationship_name) do
defp render_maybe_sensitive_attribute(
assigns,
resource,
record,
attribute,
relationship_name \\ nil
) do
assigns = assign(assigns, attribute: attribute, relationship_name: relationship_name)
show_sensitive_fields = AshAdmin.Resource.show_sensitive_fields(resource)

Expand Down Expand Up @@ -582,6 +679,38 @@ defmodule AshAdmin.Components.Resource.Show do
end
end

def handle_event("calculate", %{"calculation" => calculation} = event, socket) do
record = socket.assigns.record
domain = socket.assigns.domain

arguments =
event
|> Map.get(calculation, [])
|> Enum.map(fn {attr, value} -> {String.to_atom(attr), value} end)

calculation = String.to_atom(calculation)

calculations =
[{calculation, arguments}]

case Ash.load(
record,
calculations,
domain: domain,
actor: socket.assigns[:actor],
authorize?: socket.assigns[:authorizing]
) do
{:ok, loaded} ->
{:noreply, assign(socket, record: loaded)}

{:error, errors} ->
{:noreply,
assign(socket,
calculation_errors: Map.put(socket.assigns.calculation_errors, calculation, errors)
)}
end
end

def handle_event("load", %{"relationship" => relationship}, socket) do
record = socket.assigns.record
domain = socket.assigns.domain
Expand Down
17 changes: 17 additions & 0 deletions lib/ash_admin/resource/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ defmodule AshAdmin.Resource do
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."
]
]
}
Expand Down Expand Up @@ -190,6 +195,11 @@ defmodule AshAdmin.Resource do
end
end

def show_calculations(resource) do
Spark.Dsl.Extension.get_opt(resource, [:admin], :show_calculations, nil, true) ||
calculations(resource)
end

def fields(resource) do
Spark.Dsl.Extension.get_entities(resource, [:admin, :form])
end
Expand Down Expand Up @@ -219,4 +229,11 @@ defmodule AshAdmin.Resource do
|> Enum.sort_by(&(!Map.get(&1, :primary?)))
|> Enum.map(& &1.name)
end

defp calculations(resource) do
resource
|> Ash.Resource.Info.calculations()
|> Enum.map(& &1.name)
|> Enum.sort_by(& &1)
end
end

0 comments on commit e563097

Please sign in to comment.