improvement: remove compile-time router, use params instead

improvement!: support latest surface/phoenix
This commit is contained in:
Zach Daniel 2021-09-01 02:26:43 -04:00
parent 23de463c92
commit b214535f0c
28 changed files with 918 additions and 1207 deletions

View file

@ -11,6 +11,7 @@ locals_without_parens = [
polymorphic_tables: 1,
read_actions: 1,
relationship_display_fields: 1,
show?: 1,
show_action: 1,
table_columns: 1,
type: 1,

View file

@ -38,7 +38,7 @@ defmodule DemoWeb.Router do
pipe_through :browser
import AshAdmin.Router
ash_admin("/", apis: [Demo.Accounts.Api, Demo.Tickets.Api])
ash_admin("/")
end
end

View file

@ -1,6 +1,11 @@
defmodule Demo.Accounts.Api do
@moduledoc false
use Ash.Api
use Ash.Api,
extensions: [AshAdmin.Api]
admin do
show? true
end
resources do
resource Demo.Accounts.User

View file

@ -1,9 +1,14 @@
defmodule Demo.Tickets.Api do
@moduledoc false
use Ash.Api
use Ash.Api,
extensions: [AshAdmin.Api]
alias Demo.Tickets.{Comment, Customer, Representative, Ticket, TicketLink, Organization}
admin do
show? true
end
resources do
resource(Customer)
resource(Representative)

View file

@ -36,7 +36,7 @@ defmodule AshAdmin.ActorPlug do
session["actor_paused"]
end
actor = actor_from_session(session)
actor = actor_from_session(conn.private.phoenix_endpoint, session)
authorizing = session_bool(authorizing)
actor_paused = session_bool(actor_paused)
@ -56,37 +56,60 @@ defmodule AshAdmin.ActorPlug do
def actor_session(conn, _), do: conn
def actor_api_from_session(%{"actor_api" => api}) do
Module.concat([api])
def actor_api_from_session(endpoint, %{"actor_api" => api}) do
otp_app = endpoint.config(:otp_app)
apis = Application.get_env(otp_app, :ash_apis)
Enum.find(apis, fn allowed_api ->
AshAdmin.Api.show?(allowed_api) && AshAdmin.Api.name(allowed_api) == api
end)
end
def actor_from_session(%{
def actor_api_from_session(_, _), do: nil
def actor_from_session(endpoint, %{
"actor_resource" => resource,
"actor_api" => api,
"actor_primary_key" => primary_key,
"actor_action" => action
})
when not is_nil(resource) and not is_nil(api) do
resource = Module.concat([resource])
api = Module.concat([api])
otp_app = endpoint.config(:otp_app)
apis = Application.get_env(otp_app, :ash_apis)
action =
if action do
Ash.Resource.Info.action(resource, String.to_existing_atom(action), :read)
api =
Enum.find(apis, fn allowed_api ->
AshAdmin.Api.show?(allowed_api) && AshAdmin.Api.name(allowed_api) == api
end)
resource =
if api do
api
|> Ash.Api.resources()
|> Enum.find(fn api_resource ->
AshAdmin.Resource.name(api_resource) == resource
end)
end
case decode_primary_key(resource, primary_key) do
:error ->
nil
if api && resource do
action =
if action do
Ash.Resource.Info.action(resource, String.to_existing_atom(action), :read)
end
{:ok, filter} ->
resource
|> Ash.Query.filter(^filter)
|> api.read_one!(action: action)
case decode_primary_key(resource, primary_key) do
:error ->
nil
{:ok, filter} ->
resource
|> Ash.Query.filter(^filter)
|> api.read_one!(action: action)
end
end
end
def actor_from_session(_), do: nil
def actor_from_session(_, _), do: nil
def session_bool(value) do
case value do

View file

@ -4,7 +4,13 @@ defmodule AshAdmin.Api do
name: :admin,
schema: [
name: [
type: :string
type: :string,
doc: "The name of the api in the dashboard. Will be derived if not set."
],
show?: [
type: :boolean,
default: false,
doc: "Wether or not this api and its resources should be included in the admin dashboard"
]
]
}
@ -26,6 +32,10 @@ defmodule AshAdmin.Api do
Ash.Dsl.Extension.get_opt(api, [:admin], :name, nil, true) || default_name(api)
end
def show?(api) do
Ash.Dsl.Extension.get_opt(api, [:admin], :show?, false, true)
end
defp default_name(api) do
split = api |> Module.split()

View file

@ -7,30 +7,30 @@ defmodule AshAdmin.Components.HeroIcon do
prop(class, :css_class)
def render(assigns) do
~H"""
{{render_heroicon(@name, @type, assigns)}}
~F"""
{render_heroicon(@name, @type, assigns)}
"""
end
defp render_heroicon("minus", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
</svg>
"""
end
defp render_heroicon("plus", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
"""
end
defp render_heroicon("search-circle", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M9 9a2 2 0 114 0 2 2 0 01-4 0z" />
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a4 4 0 00-3.446 6.032l-2.261 2.26a1 1 0 101.414 1.415l2.261-2.261A4 4 0 1011 5z" clip-rule="evenodd" />
</svg>
@ -38,48 +38,48 @@ defmodule AshAdmin.Components.HeroIcon do
end
defp render_heroicon("key", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd" />
</svg>
"""
end
defp render_heroicon("check", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
"""
end
defp render_heroicon("x", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
"""
end
defp render_heroicon("information-circle", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
"""
end
defp render_heroicon("pencil", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
"""
end
defp render_heroicon("x-circle", "solid", assigns) do
~H"""
<svg class={{@class}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
~F"""
<svg class={@class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
"""

View file

@ -5,8 +5,8 @@ defmodule AshAdmin.Components.Resource.AttributeTable do
prop(resource, :any, required: true)
def render(assigns) do
~H"""
<div :if={{ Enum.any?(attributes(@resource)) }}>
~F"""
<div :if={Enum.any?(attributes(@resource))}>
<h1 class="text-center text-3xl rounded-t py-8">
Attributes
</h1>
@ -24,20 +24,20 @@ defmodule AshAdmin.Components.Resource.AttributeTable do
</thead>
<tbody>
<tr
class={{ "h-10", "bg-gray-200": rem(index, 2) == 0 }}
:for.with_index={{ {attribute, index} <- attributes(@resource) }}
class={"h-10", "bg-gray-200": rem(index, 2) == 0}
:for.with_index={{attribute, index} <- attributes(@resource)}
>
<th scope="row" class="text-center px-3">
{{ attribute.name }}
{attribute.name}
</th>
<td class="text-center px-3">
{{ attribute_type(attribute) }}
{attribute_type(attribute)}
</td>
<td class="text-center max-w-sm min-w-sm">{{ attribute.description }}</td>
<td class="text-center">{{ to_string(attribute.primary_key?) }}</td>
<td class="text-center">{{ to_string(attribute.private?) }}</td>
<td class="text-center">{{ to_string(attribute.allow_nil?) }}</td>
<td class="text-center">{{ to_string(attribute.writable?) }}</td>
<td class="text-center max-w-sm min-w-sm">{attribute.description}</td>
<td class="text-center">{to_string(attribute.primary_key?)}</td>
<td class="text-center">{to_string(attribute.private?)}</td>
<td class="text-center">{to_string(attribute.allow_nil?)}</td>
<td class="text-center">{to_string(attribute.writable?)}</td>
</tr>
</tbody>
</table>

View file

@ -144,25 +144,25 @@ defmodule AshAdmin.Components.Resource.DataTable do
end
def render(assigns) do
~H"""
~F"""
<div>
<div class="sm:mt-0 bg-gray-300 min-h-screen">
<div
:if={{ @action.arguments != [] }}
:if={@action.arguments != []}
class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:pt-10 mb-10"
>
<div class="md:mt-0 md:col-span-2">
<div class="shadow-lg overflow-hidden pt-2 sm:rounded-md bg-white">
<div class="px-4 sm:p-6">
<Form
:if={{ @query }}
as={{ :query }}
for={{ @query }}
:if={@query}
as={:query}
for={@query}
change="validate"
submit="save"
:let={{ form: form }}
:let={form: form}
>
{{ AshAdmin.Components.Resource.Form.render_attributes(assigns, @resource, @action, form) }}
{AshAdmin.Components.Resource.Form.render_attributes(assigns, @resource, @action, form)}
<div class="px-4 py-3 text-right sm:px-6">
<button
type="submit"
@ -178,46 +178,46 @@ defmodule AshAdmin.Components.Resource.DataTable do
</div>
<div
:if={{ AshAdmin.Resource.polymorphic?(@resource) }}
:if={AshAdmin.Resource.polymorphic?(@resource)}
class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:pt-10 mb-10"
>
<div class="md:mt-0 md:col-span-2">
<div class="px-4 sm:p-6">
<AshAdmin.Components.Resource.SelectTable
resource={{ @resource }}
resource={@resource}
on_change="change_table"
table={{ @table }}
tables={{ @tables }}
table={@table}
tables={@tables}
/>
</div>
</div>
</div>
<div :if={{ @action.arguments == [] || @params["args"] }} class="h-full overflow-scroll md:mx-4">
<div :if={@action.arguments == [] || @params["args"]} class="h-full overflow-scroll md:mx-4">
<div class="shadow-lg overflow-scroll sm:rounded-md bg-white">
<div :if={{ match?({:error, _}, @data) }}>
{{ {:error, %{query: query}} = @data
nil }}
<div :if={match?({:error, _}, @data)}>
{{:error, %{query: query}} = @data
nil}
<ul>
<li :for={{ error <- query.errors }}>
{{ message(error) }}
<li :for={error <- query.errors}>
{message(error)}
</li>
</ul>
</div>
<div class="px-2">
{{ render_pagination_links(assigns, :top) }}
{render_pagination_links(assigns, :top)}
<Table
:if={{ match?({:ok, _data}, @data) }}
table={{ @table }}
data={{ data(@data) }}
resource={{ @resource }}
api={{ @api }}
set_actor={{ @set_actor }}
attributes={{ AshAdmin.Resource.table_columns(@resource) }}
format_fields={{ AshAdmin.Resource.format_fields(@resource) }}
prefix={{ @prefix }}
:if={match?({:ok, _data}, @data)}
table={@table}
data={data(@data)}
resource={@resource}
api={@api}
set_actor={@set_actor}
attributes={AshAdmin.Resource.table_columns(@resource)}
format_fields={AshAdmin.Resource.format_fields(@resource)}
prefix={@prefix}
/>
{{ render_pagination_links(assigns, :bottom) }}
{render_pagination_links(assigns, :bottom)}
</div>
</div>
</div>
@ -264,36 +264,28 @@ defmodule AshAdmin.Components.Resource.DataTable do
def handle_event("change_table", %{"table" => %{"table" => table}}, socket) do
{:noreply,
push_redirect(socket,
to:
ash_action_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
socket.assigns.action.type,
socket.assigns.action.name,
table
)
to: self_path(socket.assigns.url_path, socket.assigns.params, %{"table" => table})
)}
end
defp render_pagination_links(assigns, placement) do
~H"""
~F"""
<div
:if={{ (offset?(@data) || keyset?(@data)) && show_pagination_links?(@data, placement) }}
:if={(offset?(@data) || keyset?(@data)) && show_pagination_links?(@data, placement)}
class="w-5/6 mx-auto"
>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<button
:if={{ !(keyset?(@data) && is_nil(@params["page"])) && prev_page?(@data) }}
:if={!(keyset?(@data) && is_nil(@params["page"])) && prev_page?(@data)}
:on-click="prev_page"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:text-gray-500"
>
Previous
</button>
{{ render_pagination_information(assigns, true) }}
{render_pagination_information(assigns, true)}
<button
:if={{ next_page?(@data) }}
:if={next_page?(@data)}
:on-click="next_page"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:text-gray-500"
>
@ -302,12 +294,12 @@ defmodule AshAdmin.Components.Resource.DataTable do
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
{{ render_pagination_information(assigns) }}
{render_pagination_information(assigns)}
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button
:if={{ !(keyset?(@data) && is_nil(@params["page"])) && prev_page?(@data) }}
:if={!(keyset?(@data) && is_nil(@params["page"])) && prev_page?(@data)}
:on-click="prev_page"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
>
@ -327,13 +319,13 @@ defmodule AshAdmin.Components.Resource.DataTable do
/>
</svg>
</button>
<span :if={{ offset?(@data) }}>
{{ render_page_links(assigns, leading_page_nums(@data)) }}
{{ render_middle_page_num(assigns, @page_num, trailing_page_nums(@data)) }}
{{ render_page_links(assigns, trailing_page_nums(@data)) }}
<span :if={offset?(@data)}>
{render_page_links(assigns, leading_page_nums(@data))}
{render_middle_page_num(assigns, @page_num, trailing_page_nums(@data))}
{render_page_links(assigns, trailing_page_nums(@data))}
</span>
<button
:if={{ next_page?(@data) }}
:if={next_page?(@data)}
:on-click="next_page"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
>
@ -362,33 +354,33 @@ defmodule AshAdmin.Components.Resource.DataTable do
end
defp render_page_links(assigns, page_nums) do
~H"""
~F"""
<button
:on-click="specific_page"
phx-value-page={{ i }}
:for={{ i <- page_nums }}
class={{
phx-value-page={i}
:for={i <- page_nums}
class={
"relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50",
"bg-gray-300": @page_num == i
}}
}
>
{{ i }}
{i}
</button>
"""
end
defp render_pagination_information(assigns, small? \\ false) do
~H"""
<p class={{ "text-sm text-gray-700", "sm:hidden": small? }}>
<span :if={{ offset?(@data) }}>
~F"""
<p class={"text-sm text-gray-700", "sm:hidden": small?}>
<span :if={offset?(@data)}>
Showing
<span class="font-medium">{{ first(@data) }}</span>
<span class="font-medium">{first(@data)}</span>
to
<span class="font-medium">{{ last(@data) }}</span>
<span class="font-medium">{last(@data)}</span>
of
</span>
<span :if={{ count(@data) }}>
<span class="font-medium">{{ count(@data) }}</span>
<span :if={count(@data)}>
<span class="font-medium">{count(@data)}</span>
results
</span>
</p>
@ -435,19 +427,19 @@ defmodule AshAdmin.Components.Resource.DataTable do
defp render_middle_page_num(assigns, num, trailing_page_nums) do
ellipsis? = num in trailing_page_nums || num <= 3
~H"""
~F"""
<span
:if={{ show_ellipses?(@data) }}
class={{
:if={show_ellipses?(@data)}
class={
"relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700",
"bg-gray-300": !ellipsis?
}}
}
>
<span :if={{ ellipsis? }}>
<span :if={ellipsis?}>
...
</span>
<span :if={{ !ellipsis? }}>
{{ num }}
<span :if={!ellipsis?}>
{num}
</span>
</span>
"""

View file

@ -90,23 +90,23 @@ defmodule AshAdmin.Components.Resource.Form do
end
def render(assigns) do
~H"""
~F"""
<div class="md:pt-10 sm:mt-0 bg-gray-300 min-h-screen">
<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_form(assigns) }}
{render_form(assigns)}
</div>
</div>
<div :if={{ @type != :create }} class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:mt-10">
<div :if={@type != :create} 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">
{{ AshAdmin.Components.Resource.Show.render_show(
{AshAdmin.Components.Resource.Show.render_show(
assigns,
@record,
@resource,
"Original Record",
false
) }}
)}
</div>
</div>
</div>
@ -114,61 +114,61 @@ defmodule AshAdmin.Components.Resource.Form do
end
defp render_form(assigns) do
~H"""
~F"""
<div class="shadow-lg overflow-hidden sm:rounded-md bg-white">
<div :if={{ @form.submitted_once? }} class="ml-4 mt-4 text-red-500">
<div :if={@form.submitted_once?} class="ml-4 mt-4 text-red-500">
<ul>
<li :for={{ {field, message} <- AshPhoenix.Form.errors(@form) }}>
<span :if={{field}}>
{{ to_name(field) }}:
<li :for={{field, message} <- AshPhoenix.Form.errors(@form)}>
<span :if={field}>
{to_name(field)}:
</span>
<span>
{{message}}
{message}
</span>
</li>
</ul>
</div>
<h1 class="text-lg mt-2 ml-4">
{{ String.capitalize(to_string(@action.type)) }} {{AshAdmin.Resource.name(@resource)}}
{String.capitalize(to_string(@action.type))} {AshAdmin.Resource.name(@resource)}
</h1>
<div class="flex justify-between col-span-6 mr-4 mt-2 overflow-auto px-4">
<AshAdmin.Components.Resource.SelectTable
resource={{ @resource }}
resource={@resource}
on_change="change_table"
table={{ @table }}
tables={{ @tables }}
table={@table}
tables={@tables}
/>
<Form
as={{ :action }}
for={{ :action }}
as={:action}
for={:action}
change="change_action"
opts={{id: @id <> "_action_form"}}
opts={id: @id <> "_action_form"}
>
<FieldContext name="action">
<Label>Action</Label>
<Select
opts={{disabled: Enum.count(actions(@resource, @type)) <= 1 }}
selected={{ to_string(@action.name) }} options={{ actions(@resource, @type) }} />
opts={disabled: Enum.count(actions(@resource, @type)) <= 1}
selected={to_string(@action.name)} options={actions(@resource, @type)} />
</FieldContext>
</Form>
</div>
<div class="px-4 py-5 sm:p-6">
<Form
for={{ @form }}
for={@form}
change="validate"
submit="save"
opts={{ autocomplete: false, id: @id <> "_form" }}
:let={{ form: form }}
opts={autocomplete: false, id: @id <> "_form"}
:let={form: form}
>
<input hidden phx-hook="FormChange" id="resource_form">
<input :for={{kv <- form.hidden}} name={{form.name <> "[#{elem(kv, 0)}]"}} value={{elem(kv, 1)}} hidden>
{{ render_attributes(assigns, @resource, @action, form) }}
<input :for={kv <- form.hidden} name={form.name <> "[#{elem(kv, 0)}]"} value={elem(kv, 1)} hidden>
{render_attributes(assigns, @resource, @action, form)}
<div class="px-4 py-3 text-right sm:px-6">
<button
type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{{ save_button_text(@type) }}
{save_button_text(@type)}
</button>
</div>
</Form>
@ -188,72 +188,72 @@ defmodule AshAdmin.Components.Resource.Form do
exactly \\ nil,
skip \\ []
) do
~H"""
{{ {attributes, flags, bottom_attributes, relationship_args} = attributes(resource, action, exactly)
nil }}
<Context put={{ Form, form: form }}>
~F"""
{{attributes, flags, bottom_attributes, relationship_args} = attributes(resource, action, exactly)
nil}
<Context put={Form, form: form}>
<div class="grid grid-cols-6 gap-6">
<div
:for={{ attribute <- Enum.reject(attributes, &(&1.name in skip)) }}
class={{
:for={attribute <- Enum.reject(attributes, &(&1.name in skip))}
class={
"col-span-6",
"sm:col-span-2": short_text?(resource, attribute),
"sm:col-span-3": !long_text?(resource, attribute)
}}
}
>
<FieldContext name={{ attribute.name }}>
<Label class="block text-sm font-medium text-gray-700">{{ to_name(attribute.name) }}</Label>
{{ render_attribute_input(assigns, attribute, form) }}
<ErrorTag :if={{!Ash.Type.embedded_type?(attribute.type)}} field={{ attribute.name }} />
<FieldContext name={attribute.name}>
<Label class="block text-sm font-medium text-gray-700">{to_name(attribute.name)}</Label>
{render_attribute_input(assigns, attribute, form)}
<ErrorTag :if={!Ash.Type.embedded_type?(attribute.type)} field={attribute.name} />
</FieldContext>
</div>
</div>
<div :if={{ !Enum.empty?(flags) }} class="hidden sm:block" aria-hidden="true">
<div :if={!Enum.empty?(flags)} class="hidden sm:block" aria-hidden="true">
<div class="py-5">
<div class="border-t border-gray-200" />
</div>
</div>
<div class="grid grid-cols-6 gap-6" :if={{ !Enum.empty?(flags) }}>
<div class="grid grid-cols-6 gap-6" :if={!Enum.empty?(flags)}>
<div
:for={{ attribute <- flags }}
class={{
:for={attribute <- flags}
class={
"col-span-6",
"sm:col-span-2": short_text?(resource, attribute),
"sm:col-span-3": !long_text?(resource, attribute)
}}
}
>
<FieldContext name={{ attribute.name }}>
<Label class="block text-sm font-medium text-gray-700">{{ to_name(attribute.name) }}</Label>
{{ render_attribute_input(assigns, attribute, form) }}
<ErrorTag :if={{!Ash.Type.embedded_type?(attribute.type)}} field={{ attribute.name }} />
<FieldContext name={attribute.name}>
<Label class="block text-sm font-medium text-gray-700">{to_name(attribute.name)}</Label>
{render_attribute_input(assigns, attribute, form)}
<ErrorTag :if={!Ash.Type.embedded_type?(attribute.type)} field={attribute.name} />
</FieldContext>
</div>
</div>
<div :if={{ !Enum.empty?(bottom_attributes) }} class="hidden sm:block" aria-hidden="true">
<div :if={!Enum.empty?(bottom_attributes)} class="hidden sm:block" aria-hidden="true">
<div class="py-5">
<div class="border-t border-gray-200" />
</div>
</div>
<div class="grid grid-cols-6 gap-6" :if={{ !Enum.empty?(bottom_attributes) }}>
<div class="grid grid-cols-6 gap-6" :if={!Enum.empty?(bottom_attributes)}>
<div
:for={{ attribute <- bottom_attributes }}
class={{
:for={attribute <- bottom_attributes}
class={
"col-span-6",
"sm:col-span-2": short_text?(resource, attribute),
"sm:col-span-3": !(long_text?(resource, attribute) || Ash.Type.embedded_type?(attribute.type))
}}
}
>
<FieldContext name={{ attribute.name }}>
<Label class="block text-sm font-medium text-gray-700">{{ to_name(attribute.name) }}</Label>
{{ render_attribute_input(assigns, attribute, form) }}
<ErrorTag :if={{!Ash.Type.embedded_type?(attribute.type)}} field={{ attribute.name }} />
<FieldContext name={attribute.name}>
<Label class="block text-sm font-medium text-gray-700">{to_name(attribute.name)}</Label>
{render_attribute_input(assigns, attribute, form)}
<ErrorTag :if={!Ash.Type.embedded_type?(attribute.type)} field={attribute.name} />
</FieldContext>
</div>
</div>
<div :for={{{relationship, argument, opts} <- relationship_args}}>
<FieldContext name={{argument.name}} :if={{relationship not in skip and argument.name not in skip}}>
<Label class="block text-sm font-medium text-gray-700">{{ to_name(argument.name)}}</Label>
{{ render_relationship_input(assigns, Ash.Resource.Info.relationship(form.source.resource, relationship), form, argument, opts) }}
<div :for={{relationship, argument, opts} <- relationship_args}>
<FieldContext name={argument.name} :if={relationship not in skip and argument.name not in skip}>
<Label class="block text-sm font-medium text-gray-700">{to_name(argument.name)}</Label>
{render_relationship_input(assigns, Ash.Resource.Info.relationship(form.source.resource, relationship), form, argument, opts)}
</FieldContext>
</div>
</Context>
@ -267,39 +267,39 @@ defmodule AshAdmin.Components.Resource.Form do
argument,
opts
) do
~H"""
<div :if={{ !needs_to_load?(opts) || loaded?(form.source.source, relationship.name) }}>
~F"""
<div :if={!needs_to_load?(opts) || loaded?(form.source.source, relationship.name)}>
<Inputs
form={{ form }}
for={{ argument.name }}
:let={{ form: inner_form }}
form={form}
for={argument.name}
:let={form: inner_form}
>
<div :if={{ @form.submitted_once? }} class="ml-4 mt-4 text-red-500">
<div :if={@form.submitted_once?} class="ml-4 mt-4 text-red-500">
<ul>
<li :for={{ {field, message} <- AshPhoenix.Form.errors(@form, inner_form.name) }}>
<span :if={{field}}>
{{ to_name(field) }}:
<li :for={{field, message} <- AshPhoenix.Form.errors(@form, inner_form.name)}>
<span :if={field}>
{to_name(field)}:
</span>
<span>
{{message}}
{message}
</span>
</li>
</ul>
</div>
<input :for={{kv <- inner_form.hidden}} name={{inner_form.name <> "[#{elem(kv, 0)}]"}} value={{elem(kv, 1)}} hidden>
{{ render_attributes(
<input :for={kv <- inner_form.hidden} name={inner_form.name <> "[#{elem(kv, 0)}]"} value={elem(kv, 1)} hidden>
{render_attributes(
assigns,
inner_form.source.resource,
inner_form.source.source.action,
inner_form,
relationship_fields(inner_form),
skip_related(relationship)
) }}
)}
<button
type="button"
:on-click="remove_form"
:if={{can_remove_related?(inner_form, opts)}}
phx-value-path={{ inner_form.name }}
:if={can_remove_related?(inner_form, opts)}
phx-value-path={inner_form.name}
class="flex h-6 w-6 mt-2 border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
<HeroIcon name="minus" class="h-4 w-4 text-gray-500" />
@ -309,9 +309,9 @@ defmodule AshAdmin.Components.Resource.Form do
<button
type="button"
:on-click="add_form"
:if={{ can_add_related?(form, :read_action, argument)}}
phx-value-path={{ form.name <> "[#{argument.name}]" }}
phx-value-type={{ "lookup" }}
:if={can_add_related?(form, :read_action, argument)}
phx-value-path={form.name <> "[#{argument.name}]"}
phx-value-type={"lookup"}
class="flex h-6 w-6 m-2 border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
<HeroIcon name="search-circle" class="h-4 w-4 text-gray-500" />
@ -320,9 +320,9 @@ defmodule AshAdmin.Components.Resource.Form do
<button
type="button"
:on-click="add_form"
:if={{ can_add_related?(form, :create_action, argument) }}
phx-value-path={{ form.name <> "[#{argument.name}]" }}
phx-value-type={{"create"}}
:if={can_add_related?(form, :create_action, argument)}
phx-value-path={form.name <> "[#{argument.name}]"}
phx-value-type={"create"}
class="flex h-6 w-6 m-2 border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
<HeroIcon name="plus" class="h-4 w-4 text-gray-500" />
@ -330,29 +330,29 @@ defmodule AshAdmin.Components.Resource.Form do
<button
type="button"
:on-click="add_form"
:if={{ form.source.form_keys[argument.name][:read_form] && !relationship_set?(form.source.source, relationship.name, argument.name) }}
phx-value-path={{ form.name <> "[#{argument.name}]" }}
phx-value-type={{ "lookup" }}
:if={form.source.form_keys[argument.name][:read_form] && !relationship_set?(form.source.source, relationship.name, argument.name)}
phx-value-path={form.name <> "[#{argument.name}]"}
phx-value-type={"lookup"}
class="flex h-6 w-6 m-2 border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
<HeroIcon name="plus" class="h-4 w-4 text-gray-500" />
</button>
</div>
<div :if={{ needs_to_load?(opts) && !loaded?(form.source.source, relationship.name) }}>
<div :if={needs_to_load?(opts) && !loaded?(form.source.source, relationship.name)}>
<button
:on-click="load"
phx-value-path={{form.name}}
phx-value-relationship={{relationship.name}}
phx-value-path={form.name}
phx-value-relationship={relationship.name}
type="button"
class="flex py-2 ml-4 px-4 mt-2 bg-indigo-600 text-white border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
Load
</button>
<div :if={{ is_exception(@load_errors[relationship.name]) }}>
{{ Exception.message(@load_errors[relationship.name]) }}
<div :if={is_exception(@load_errors[relationship.name])}>
{Exception.message(@load_errors[relationship.name])}
</div>
<div :if={{ @load_errors[relationship.name] && !is_exception(@load_errors[relationship.name]) }}>
{{ inspect(@load_errors[relationship.name]) }}
<div :if={@load_errors[relationship.name] && !is_exception(@load_errors[relationship.name])}>
{inspect(@load_errors[relationship.name])}
</div>
</div>
"""
@ -478,13 +478,13 @@ defmodule AshAdmin.Components.Resource.Form do
value,
name
) do
~H"""
~F"""
<Checkbox
form={{ form }}
value={{value(value, form, attribute)}}
name={{name || form.name <> "[#{attribute.name}]"}}
form={form}
value={value(value, form, attribute)}
name={name || form.name <> "[#{attribute.name}]"}
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
:props={{props(value, attribute)}}
:props={props(value, attribute)}
/>
"""
end
@ -498,13 +498,13 @@ defmodule AshAdmin.Components.Resource.Form do
value,
name
) do
~H"""
~F"""
<Select
form={{ form }}
options={{ Nil: nil, True: "true", False: "false" }}
selected={{value(value, form, attribute)}}
name={{name || form.name <> "[#{attribute.name}]"}}
:props={{props(value, attribute)}} />
form={form}
options={Nil: nil, True: "true", False: "false"}
selected={value(value, form, attribute)}
name={name || form.name <> "[#{attribute.name}]"}
:props={props(value, attribute)} />
"""
end
@ -521,54 +521,54 @@ defmodule AshAdmin.Components.Resource.Form do
when type in [Ash.Type.CiString, Ash.Type.String, Ash.Type.UUID, Ash.Type.Atom] do
cond do
type == Ash.Type.Atom && attribute.constraints[:one_of] ->
~H"""
~F"""
<Select
form={{ form }}
:props={{props(value, attribute)}}
options={{ Enum.map(attribute.constraints[:one_of], &{to_name(&1), &1}) ++ allow_nil_option(attribute) }}
selected={{value(value, form, attribute)}}
name={{name || form.name <> "[#{attribute.name}]"}}
form={form}
:props={props(value, attribute)}
options={Enum.map(attribute.constraints[:one_of], &{to_name(&1), &1}) ++ allow_nil_option(attribute)}
selected={value(value, form, attribute)}
name={name || form.name <> "[#{attribute.name}]"}
/>
"""
long_text?(form.source.resource, attribute) ->
~H"""
~F"""
<TextArea
form={{ form }}
:props={{props(value, attribute)}}
name={{name || form.name <> "[#{attribute.name}]"}}
opts={{
form={form}
:props={props(value, attribute)}
name={name || form.name <> "[#{attribute.name}]"}
opts={
type: text_input_type(attribute),
placeholder: placeholder(default),
phx_hook: "MaintainAttrs",
data_attrs: "style"
}}
value={{value(value, form, attribute)}}
}
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 resize-y"
/>
"""
short_text?(form.source.resource, attribute) ->
~H"""
~F"""
<TextInput
form={{ form }}
:props={{props(value, attribute)}}
opts={{ type: text_input_type(attribute), placeholder: placeholder(default) }}
value={{value(value, form, attribute)}}
form={form}
:props={props(value, attribute)}
opts={type: text_input_type(attribute), placeholder: placeholder(default)}
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"
name={{name || form.name <> "[#{attribute.name}]"}}
name={name || form.name <> "[#{attribute.name}]"}
/>
"""
true ->
~H"""
~F"""
<TextInput
form={{ form }}
:props={{props(value, attribute)}}
opts={{ type: text_input_type(attribute), placeholder: placeholder(default) }}
value={{value(value, form, attribute)}}
form={form}
:props={props(value, attribute)}
opts={type: text_input_type(attribute), placeholder: placeholder(default)}
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"
name={{name || form.name <> "[#{attribute.name}]"}}
name={name || form.name <> "[#{attribute.name}]"}
/>
"""
end
@ -587,33 +587,33 @@ defmodule AshAdmin.Components.Resource.Form do
def render_attribute_input(assigns, %{type: Ash.Type.Map} = attribute, form, value, name) do
encoded = Jason.encode!(value(value, form, attribute))
~H"""
~F"""
<div>
<div
phx-hook="JsonEditor"
phx-update="ignore"
data-input-id={{form.id <> "_#{attribute.name}"}}
id={{form.id <> "_#{attribute.name}_json"}}
data-input-id={form.id <> "_#{attribute.name}"}
id={form.id <> "_#{attribute.name}_json"}
/>
<HiddenInput
opts={{phx_hook: "JsonEditorSource", data_editor_id: form.id <> "_#{attribute.name}_json"}}
form={{ form }}
value={{encoded}}
name={{name || form.name <> "[#{attribute.name}]"}}
id={{form.id <> "_#{attribute.name}"}}
opts={phx_hook: "JsonEditorSource", data_editor_id: form.id <> "_#{attribute.name}_json"}
form={form}
value={encoded}
name={name || form.name <> "[#{attribute.name}]"}
id={form.id <> "_#{attribute.name}"}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
</div>
"""
rescue
_ ->
~H"""
~F"""
<TextInput
form={{ form }}
opts={{ disabled: true }}
value={{"..."}}
name={{name || form.name <> "[#{attribute.name}]"}}
form={form}
opts={disabled: true}
value={"..."}
name={name || form.name <> "[#{attribute.name}]"}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
"""
@ -622,26 +622,26 @@ defmodule AshAdmin.Components.Resource.Form do
def render_attribute_input(assigns, attribute, form, value, name) do
cond do
Ash.Type.embedded_type?(attribute.type) ->
~H"""
<Inputs form={{ form }} for={{ attribute.name }} :let={{ form: inner_form }}>
<input :for={{kv <- inner_form.hidden}} name={{inner_form.name <> "[#{elem(kv, 0)}]"}} value={{elem(kv, 1)}} hidden>
~F"""
<Inputs form={form} for={attribute.name} :let={form: inner_form}>
<input :for={kv <- inner_form.hidden} name={inner_form.name <> "[#{elem(kv, 0)}]"} value={elem(kv, 1)} hidden>
<button
type="button"
:on-click="remove_form"
phx-value-path={{ inner_form.name }}
phx-value-path={inner_form.name}
class="flex h-6 w-6 mt-2 border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
<HeroIcon name="minus" class="h-4 w-4 text-gray-500" />
</button>
{{ render_attributes(assigns, inner_form.source.resource, inner_form.source.source.action, inner_form) }}
{render_attributes(assigns, inner_form.source.resource, inner_form.source.source.action, inner_form)}
</Inputs>
<button
type="button"
:on-click="add_form"
:if={{can_append_embed?(form.source.source, attribute.name)}}
phx-value-pkey={{embedded_type_pkey(attribute.type)}}
phx-value-path={{ name || form.name <> "[#{attribute.name}]" }}
:if={can_append_embed?(form.source.source, attribute.name)}
phx-value-pkey={embedded_type_pkey(attribute.type)}
phx-value-path={name || form.name <> "[#{attribute.name}]"}
class="flex h-6 w-6 mt-2 border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
<HeroIcon name="plus" class="h-4 w-4 text-gray-500" />
@ -649,13 +649,13 @@ defmodule AshAdmin.Components.Resource.Form do
"""
is_atom(attribute.type) && :erlang.function_exported(attribute.type, :values, 0) ->
~H"""
~F"""
<Select
form={{ form }}
:props={{props(value, attribute)}}
options={{ Enum.map(attribute.type.values(), &{to_name(&1), &1}) ++ allow_nil_option(attribute) }}
selected={{value(value, form, attribute)}}
name={{name || form.name <> "[#{attribute.name}]"}}
form={form}
:props={props(value, attribute)}
options={Enum.map(attribute.type.values(), &{to_name(&1), &1}) ++ allow_nil_option(attribute)}
selected={value(value, form, attribute)}
name={name || form.name <> "[#{attribute.name}]"}
/>
"""
@ -667,16 +667,16 @@ defmodule AshAdmin.Components.Resource.Form do
defp render_fallback_attribute(assigns, form, %{type: {:array, type}} = attribute, value, name) do
name = name || form.name <> "[#{attribute.name}]"
~H"""
~F"""
<div>
<div :for.with_index={{{value, index} <- list_value(value || Phoenix.HTML.FormData.input_value(form.source, form, attribute.name))}}>
{{render_attribute_input(assigns, %{attribute | type: type, constraints: attribute.constraints[:items] || []}, %{form | params: %{"#{attribute.name}" => form.params["#{attribute.name}"]["#{index}"]}}, {:value, value}, name <> "[#{index}]")}}
<div :for.with_index={{value, index} <- list_value(value || Phoenix.HTML.FormData.input_value(form.source, form, attribute.name))}>
{render_attribute_input(assigns, %{attribute | type: type, constraints: attribute.constraints[:items] || []}, %{form | params: %{"#{attribute.name}" => form.params["#{attribute.name}"]["#{index}"]}}, {:value, value}, name <> "[#{index}]")}
<button
type="button"
:on-click="remove_value"
phx-value-path={{ form.name }}
phx-value-field={{ attribute.name}}
phx-value-index={{ index}}
phx-value-path={form.name}
phx-value-field={attribute.name}
phx-value-index={index}
class="flex h-6 w-6 mt-2 border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
<HeroIcon name="minus" class="h-4 w-4 text-gray-500" />
@ -685,8 +685,8 @@ defmodule AshAdmin.Components.Resource.Form do
<button
type="button"
:on-click="append_value"
phx-value-path={{ form.name }}
phx-value-field={{ attribute.name }}
phx-value-path={form.name}
phx-value-field={attribute.name}
class="flex h-6 w-6 mt-2 border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
<HeroIcon name="plus" class="h-4 w-4 text-gray-500" />
@ -698,12 +698,12 @@ defmodule AshAdmin.Components.Resource.Form do
defp render_fallback_attribute(assigns, form, attribute, value, name) do
casted_value = Phoenix.HTML.Safe.to_iodata(value(value, form, attribute))
~H"""
~F"""
<TextInput
form={{ form }}
opts={{ type: text_input_type(attribute), placeholder: placeholder(attribute.default) }}
value={{casted_value}}
name={{name || form.name <> "[#{attribute.name}]"}}
form={form}
opts={type: text_input_type(attribute), placeholder: placeholder(attribute.default)}
value={casted_value}
name={name || form.name <> "[#{attribute.name}]"}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
"""
@ -711,23 +711,23 @@ defmodule AshAdmin.Components.Resource.Form do
_ ->
case Map.fetch(form.params, to_string(attribute.name)) do
{:ok, value} ->
~H"""
~F"""
<TextInput
form={{ form }}
opts={{ type: text_input_type(attribute), placeholder: placeholder(attribute.default) }}
value={{value}}
name={{name || form.name <> "[#{attribute.name}]"}}
form={form}
opts={type: text_input_type(attribute), placeholder: placeholder(attribute.default)}
value={value}
name={name || form.name <> "[#{attribute.name}]"}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
"""
:error ->
~H"""
~F"""
<TextInput
form={{ form }}
opts={{ disabled: true }}
value={{"..."}}
name={{name || form.name <> "[#{attribute.name}]"}}
form={form}
opts={disabled: true}
value={"..."}
name={name || form.name <> "[#{attribute.name}]"}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
"""
@ -827,13 +827,7 @@ defmodule AshAdmin.Components.Resource.Form do
socket
|> redirect(
to:
ash_show_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
record,
socket.assigns.table
)
"#{socket.assigns.prefix}?api=#{AshAdmin.Api.name(socket.assigns.api)}&resource=#{AshAdmin.Resource.name(socket.assigns.resource)}&tab=show&table=#{socket.assigns.table}&primary_key=#{encode_primary_key(record)}"
)}
else
case Ash.Resource.Info.primary_action(socket.assigns.resource, :update) do
@ -841,22 +835,15 @@ defmodule AshAdmin.Components.Resource.Form do
{:noreply,
redirect(socket,
to:
ash_admin_path(socket.assigns.prefix, socket.assigns.api, socket.assigns.resource)
"#{socket.assigns.prefix}?api=#{AshAdmin.Api.name(socket.assigns.api)}&resource=#{AshAdmin.Resource.name(socket.assigns.resource)}"
)}
update ->
_update ->
{:noreply,
socket
|> redirect(
to:
ash_update_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
record,
update,
socket.assigns.table
)
"#{socket.assigns.prefix}?api=#{AshAdmin.Api.name(socket.assigns.api)}&resource=#{AshAdmin.Resource.name(socket.assigns.resource)}&action_type=update&tab=update&table=#{socket.assigns.table}&primary_key=#{encode_primary_key(record)}"
)}
end
end
@ -867,42 +854,19 @@ defmodule AshAdmin.Components.Resource.Form do
:create ->
{:noreply,
push_redirect(socket,
to:
ash_create_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
socket.assigns.action.name,
table
)
to: self_path(socket.assigns.url_path, socket.assigns.params, %{"table" => table})
)}
:update ->
{:noreply,
push_redirect(socket,
to:
ash_update_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
socket.assigns.record,
socket.assigns.action.name,
table
)
to: self_path(socket.assigns.url_path, socket.assigns.params, %{"table" => table})
)}
:destroy ->
{:noreply,
push_redirect(socket,
to:
ash_destroy_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
socket.assigns.record,
socket.assigns.action.name,
table
)
to: self_path(socket.assigns.url_path, socket.assigns.params, %{"table" => table})
)}
end
end
@ -926,42 +890,19 @@ defmodule AshAdmin.Components.Resource.Form do
:create ->
{:noreply,
push_redirect(socket,
to:
ash_create_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
action.name,
socket.assigns.table
)
to: self_path(socket.assigns.url_path, socket.assigns.params, %{"action" => action})
)}
:update ->
{:noreply,
push_redirect(socket,
to:
ash_update_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
socket.assigns.record,
action.name,
socket.assigns.table
)
to: self_path(socket.assigns.url_path, socket.assigns.params, %{"action" => action})
)}
:destroy ->
{:noreply,
push_redirect(socket,
to:
ash_destroy_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource,
socket.assigns.record,
action.name,
socket.assigns.table
)
to: self_path(socket.assigns.url_path, socket.assigns.params, %{"action" => action})
)}
end
end
@ -1123,11 +1064,7 @@ defmodule AshAdmin.Components.Resource.Form do
socket
|> redirect(
to:
ash_admin_path(
socket.assigns.prefix,
socket.assigns.api,
socket.assigns.resource
)
"#{socket.assigns.prefix}?api=#{AshAdmin.Api.name(socket.assigns.api)}&resource=#{AshAdmin.Resource.name(socket.assigns.resource)}"
)}
{:error, form} ->

View file

@ -3,7 +3,6 @@ defmodule AshAdmin.Components.Resource.Nav do
use Surface.Component
alias Surface.Components.LiveRedirect
alias AshAdmin.Components.TopNav.Dropdown
import AshAdmin.Helpers
prop(resource, :any, required: true)
prop(api, :any, required: true)
@ -13,29 +12,23 @@ defmodule AshAdmin.Components.Resource.Nav do
prop(prefix, :any, default: nil)
def render(assigns) do
~H"""
~F"""
<nav class="bg-gray-800 w-full">
<div class="px-4 sm:px-6 lg:px-8 w-full">
<div class="flex items-center justify-between h-16 w-full">
<div class="flex items-center w-full">
<div class="flex-shrink-0">
<h3 class="text-white text-lg">
<LiveRedirect to={{ ash_admin_path(@prefix, @api, @resource) }}>
{{ AshAdmin.Resource.name(@resource) }}
<LiveRedirect to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(@resource)}"}>
{AshAdmin.Resource.name(@resource)}
</LiveRedirect>
</h3>
</div>
<div class="w-full">
<div class="ml-12 flex items-center space-x-1">
<div :if={{ has_create_action?(@resource) }} class="relative">
<div :if={has_create_action?(@resource)} class="relative">
<LiveRedirect
to={{ash_create_path(
@prefix,
@api,
@resource,
Ash.Resource.Info.primary_action(@resource, :create).name,
@table
)}}
to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(@resource)}&action_type=create&action=#{Ash.Resource.Info.primary_action(@resource, :create).name}&tab=create&table=#{@table}"}
class="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500"
>
Create
@ -44,9 +37,9 @@ defmodule AshAdmin.Components.Resource.Nav do
<Dropdown
name="Read"
id={{ "#{@resource}_data_dropdown" }}
active={{ @tab == "data" }}
groups={{ data_groups(@prefix, @api, @resource, @action, @table) }}
id={"#{@resource}_data_dropdown"}
active={@tab == "data"}
groups={data_groups(@prefix, @api, @resource, @action, @table)}
/>
</div>
</div>
@ -68,7 +61,8 @@ defmodule AshAdmin.Components.Resource.Nav do
|> Enum.map(fn action ->
%{
text: action_name(action),
to: ash_action_path(prefix, api, resource, :read, action.name, table),
to:
"#{prefix}?api=#{AshAdmin.Api.name(api)}&resource=#{AshAdmin.Resource.name(resource)}&table=#{table}&action_type=read&action=#{action.name}",
active: current_action == action
}
end)

View file

@ -1,7 +1,6 @@
defmodule AshAdmin.Components.Resource.RelationshipTable do
@moduledoc false
use Surface.Component
import AshAdmin.Helpers
alias Surface.Components.LiveRedirect
prop(resource, :any, required: true)
@ -9,8 +8,8 @@ defmodule AshAdmin.Components.Resource.RelationshipTable do
prop(prefix, :any, required: true)
def render(assigns) do
~H"""
<div class="w-full" :if={{ Enum.any?(relationships(@resource)) }}>
~F"""
<div class="w-full" :if={Enum.any?(relationships(@resource))}>
<h1 class="text-center text-3xl rounded-t py-8">
Relationships
</h1>
@ -25,20 +24,20 @@ defmodule AshAdmin.Components.Resource.RelationshipTable do
</thead>
<tbody>
<tr
class={{ "h-10", "bg-gray-200": rem(index, 2) == 0 }}
:for.with_index={{ {relationship, index} <- relationships(@resource) }}
class={"h-10", "bg-gray-200": rem(index, 2) == 0}
:for.with_index={{relationship, index} <- relationships(@resource)}
>
<th scope="row">
{{ relationship.name }}
{relationship.name}
</th>
<td class="text-center">
{{ relationship.type }}</td>
{relationship.type}</td>
<td class="text-center">
<LiveRedirect to={{ ash_admin_path(@prefix, @api, relationship.destination) }}>
{{ AshAdmin.Resource.name(relationship.destination) }}
<LiveRedirect to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(relationship.destination)}"}>
{AshAdmin.Resource.name(relationship.destination)}
</LiveRedirect>
</td>
<td class="text-center max-w-sm min-w-sm">{{ relationship.description }}</td>
<td class="text-center max-w-sm min-w-sm">{relationship.description}</td>
</tr>
</tbody>
</table>

View file

@ -22,106 +22,107 @@ defmodule AshAdmin.Components.Resource do
prop(table, :any, default: nil)
prop(tables, :any, default: nil)
prop(prefix, :any, default: nil)
prop(action_type, :atom)
data(filter_open, :boolean, default: false)
slot(default)
def render(assigns) do
~H"""
~F"""
<div class="content-center h-screen">
<Nav
resource={{ @resource }}
api={{ @api }}
tab={{ @tab }}
action={{ @action }}
table={{ @table }}
prefix={{ @prefix }}
resource={@resource}
api={@api}
tab={@tab}
action={@action}
table={@table}
prefix={@prefix}
/>
<div class="mx-24 relative grid grid-cols-1 justify-items-center">
</div>
<slot />
<div :if={{ @record && match?({:ok, record} when not is_nil(record), @record) && @tab == "update" }}>
{{ {:ok, record} = @record
nil }}
<#slot />
<div :if={@record && match?({:ok, record} when not is_nil(record), @record) && @tab == "update"}>
{{:ok, record} = @record
nil}
<Form
type={{ :update }}
record={{ record }}
resource={{ @resource }}
action={{ @action }}
api={{ @api }}
id={{ update_id(@resource) }}
actor={{ @actor }}
set_actor={{ @set_actor }}
authorizing={{ @authorizing }}
tenant={{ @tenant }}
table={{ @table }}
tables={{ @tables }}
prefix={{ @prefix }}
type={:update}
record={record}
resource={@resource}
action={@action}
api={@api}
id={update_id(@resource)}
actor={@actor}
set_actor={@set_actor}
authorizing={@authorizing}
tenant={@tenant}
table={@table}
tables={@tables}
prefix={@prefix}
/>
</div>
<div :if={{ @record && match?({:ok, record} when not is_nil(record), @record) && @tab == "destroy" }}>
{{ {:ok, record} = @record
nil }}
<div :if={@record && match?({:ok, record} when not is_nil(record), @record) && @tab == "destroy"}>
{{:ok, record} = @record
nil}
<Form
type={{ :destroy }}
record={{ record }}
resource={{ @resource }}
action={{ @action }}
set_actor={{ @set_actor }}
api={{ @api }}
id={{ destroy_id(@resource) }}
actor={{ @actor }}
authorizing={{ @authorizing }}
tenant={{ @tenant }}
table={{ @table }}
tables={{ @tables }}
prefix={{ @prefix }}
type={:destroy}
record={record}
resource={@resource}
action={@action}
set_actor={@set_actor}
api={@api}
id={destroy_id(@resource)}
actor={@actor}
authorizing={@authorizing}
tenant={@tenant}
table={@table}
tables={@tables}
prefix={@prefix}
/>
</div>
<Show
:if={{ @tab == "read" && match?({:ok, %_{}}, @record) }}
resource={{ @resource }}
api={{ @api }}
id={{ show_id(@resource) }}
record={{ unwrap(@record) }}
actor={{ @actor }}
authorizing={{ @authorizing }}
tenant={{ @tenant }}
set_actor={{ @set_actor }}
table={{ @table }}
prefix={{ @prefix }}
:if={@tab == "show" && match?({:ok, %_{}}, @record)}
resource={@resource}
api={@api}
id={show_id(@resource)}
record={unwrap(@record)}
actor={@actor}
authorizing={@authorizing}
tenant={@tenant}
set_actor={@set_actor}
table={@table}
prefix={@prefix}
/>
<Info :if={{ @tab == "info" }} resource={{ @resource }} api={{ @api }} prefix={{ @prefix }} />
<Info :if={@tab == "info" || (is_nil(@tab) && is_nil(@action_type))} resource={@resource} api={@api} prefix={@prefix} />
<Form
:if={{ @tab == "create" }}
type={{ :create }}
resource={{ @resource }}
api={{ @api }}
set_actor={{ @set_actor }}
action={{ @action }}
id={{ create_id(@resource) }}
actor={{ @actor }}
authorizing={{ @authorizing }}
tenant={{ @tenant }}
table={{ @table }}
tables={{ @tables }}
prefix={{ @prefix }}
:if={@tab == "create"}
type={:create}
resource={@resource}
api={@api}
set_actor={@set_actor}
action={@action}
id={create_id(@resource)}
actor={@actor}
authorizing={@authorizing}
tenant={@tenant}
table={@table}
tables={@tables}
prefix={@prefix}
/>
<DataTable
:if={{ @tab == "data" }}
resource={{ @resource }}
action={{ @action }}
actor={{ @actor }}
api={{ @api }}
url_path={{ @url_path }}
params={{ @params }}
set_actor={{ @set_actor }}
id={{ data_table_id(@resource) }}
authorizing={{ @authorizing }}
table={{ @table }}
tables={{ @tables }}
prefix={{ @prefix }}
tenant={{ @tenant }}
:if={@action_type == :read && @tab != "show"}
resource={@resource}
action={@action}
actor={@actor}
api={@api}
url_path={@url_path}
params={@params}
set_actor={@set_actor}
id={data_table_id(@resource)}
authorizing={@authorizing}
table={@table}
tables={@tables}
prefix={@prefix}
tenant={@tenant}
/>
</div>
"""

View file

@ -9,10 +9,10 @@ defmodule AshAdmin.Components.Resource.Info do
prop(prefix, :any, required: true)
def render(assigns) do
~H"""
~F"""
<div class="relative mx-12">
<AttributeTable resource={{ @resource }} />
<RelationshipTable api={{ @api }} resource={{ @resource }} prefix={{ @prefix }} />
<AttributeTable resource={@resource} />
<RelationshipTable api={@api} resource={@resource} prefix={@prefix} />
</div>
"""
end

View file

@ -11,13 +11,13 @@ defmodule AshAdmin.Components.Resource.SelectTable do
prop(tables, :any, required: true)
def render(assigns) do
~H"""
~F"""
<div>
<div :if={{ @resource && AshAdmin.Resource.polymorphic?(@resource) }}>
<Form as={{ :table }} for={{ :table }} change={{ @on_change }}>
<div :if={@resource && AshAdmin.Resource.polymorphic?(@resource)}>
<Form as={:table} for={:table} change={@on_change}>
<FieldContext name="table">
<Label>Table</Label>
<Select selected={{ @table }} options={{ @tables || [] }} />
<Select selected={@table} options={@tables || []} />
</FieldContext>
</Form>
</div>

View file

@ -21,16 +21,16 @@ defmodule AshAdmin.Components.Resource.Show do
data(load_errors, :map, default: %{})
def render(assigns) do
~H"""
~F"""
<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) }}
{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_relationships(assigns, @record, @resource) }}
{render_relationships(assigns, @record, @resource)}
</div>
</div>
</div>
@ -38,48 +38,34 @@ defmodule AshAdmin.Components.Resource.Show do
end
def render_show(assigns, record, resource, title \\ nil, buttons? \\ true) do
~H"""
~F"""
<div class="shadow-lg overflow-hidden sm:rounded-md bg-white">
<h1 :if={{ title }} class="pt-2 pl-4 text-lg">{{ title }}</h1>
<h1 :if={title} class="pt-2 pl-4 text-lg">{title}</h1>
<button
:if={{ AshAdmin.Resource.actor?(@resource) }}
:if={AshAdmin.Resource.actor?(@resource)}
class="float-right pt-4 pr-4"
:on-click={{ @set_actor }}
phx-value-resource={{ @resource }}
phx-value-api={{ @api }}
phx-value-pkey={{ encode_primary_key(@record) }}
:on-click={@set_actor}
phx-value-resource={@resource}
phx-value-api={@api}
phx-value-pkey={encode_primary_key(@record)}
>
<HeroIcon name="key" class="h-5 w-5 text-gray-500" />
</button>
<div class="px-4 py-5 sm:p-6">
<div>
{{ render_attributes(assigns, record, resource) }}
<div :if={{ buttons? }} class="px-4 py-3 text-right sm:px-6">
{render_attributes(assigns, record, resource)}
<div :if={buttons?} class="px-4 py-3 text-right sm:px-6">
<LiveRedirect
to={{ash_destroy_path(
@prefix,
@api,
@resource,
@record,
Ash.Resource.Info.primary_action(@resource, :destroy).name,
@table
)}}
:if={{ destroy?(@resource) }}
to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(@resource)}&action_type=destroy&action=#{Ash.Resource.Info.primary_action(@resource, :destroy).name}&tab=destroy&table=#{@table}&primary_key=#{encode_primary_key(@record)}"}
:if={destroy?(@resource)}
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Destroy
</LiveRedirect>
<LiveRedirect
to={{ash_update_path(
@prefix,
@api,
@resource,
@record,
Ash.Resource.Info.primary_action(@resource, :update).name,
@table
)}}
:if={{ update?(@resource) }}
to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(@resource)}&action_type=update&action=#{Ash.Resource.Info.primary_action(@resource, :update).name}&tab=update&table=#{@table}&primary_key=#{encode_primary_key(@record)}"}
:if={update?(@resource)}
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Update
@ -92,35 +78,35 @@ defmodule AshAdmin.Components.Resource.Show do
end
defp render_relationships(assigns, _record, resource) do
~H"""
~F"""
<div
:for={{ relationship <- AshAdmin.Components.Resource.Form.relationships(resource, :show) }}
:for={relationship <- AshAdmin.Components.Resource.Form.relationships(resource, :show)}
class="shadow-lg overflow-hidden sm:rounded-md mb-2 bg-white"
>
<div class="px-4 py-5 mt-2">
<div>
{{ to_name(relationship.name) }}
{to_name(relationship.name)}
<button
:if={{ !loaded?(@record, relationship.name) }}
:if={!loaded?(@record, relationship.name)}
:on-click="load"
phx-value-relationship={{ relationship.name }}
phx-value-relationship={relationship.name}
type="button"
class="flex py-2 ml-4 px-4 mt-2 bg-indigo-600 text-white border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
Load
</button>
<button
:if={{ loaded?(@record, relationship.name) && relationship.cardinality == :many }}
:if={loaded?(@record, relationship.name) && relationship.cardinality == :many}
:on-click="unload"
phx-value-relationship={{ relationship.name }}
phx-value-relationship={relationship.name}
type="button"
class="flex py-2 ml-4 px-4 mt-2 bg-indigo-600 text-white border-gray-600 hover:bg-gray-400 rounded-md justify-center items-center"
>
Unload
</button>
<div :if={{ loaded?(@record, relationship.name) }}>
{{ render_relationship_data(assigns, @record, relationship) }}
<div :if={loaded?(@record, relationship.name)}>
{render_relationship_data(assigns, @record, relationship)}
</div>
</div>
</div>
@ -136,22 +122,16 @@ defmodule AshAdmin.Components.Resource.Show do
}) do
case Map.get(record, name) do
nil ->
~H"None"
~F"None"
record ->
~H"""
~F"""
<div class="mb-10">
{{ render_attributes(assigns, record, destination) }}
{render_attributes(assigns, record, destination)}
<div class="px-4 py-3 text-right sm:px-6">
<LiveRedirect
:if={{ AshAdmin.Resource.show_action(destination) }}
to={{ash_show_path(
@prefix,
@api,
destination,
record,
context[:data_layer][:table]
)}}
:if={AshAdmin.Resource.show_action(destination)}
to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(@resource)}&tab=show&table=#{context[:data_layer][:table]}&primary_key=#{encode_primary_key(@record)}"}
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Show
@ -171,74 +151,74 @@ defmodule AshAdmin.Components.Resource.Show do
}) do
data = Map.get(record, name)
~H"""
~F"""
<div class="mb-10 overflow-scroll">
<Table
data={{ data }}
resource={{ destination }}
api={{ @api }}
set_actor={{ @set_actor }}
table={{ context[:data_layer][:table] }}
prefix={{ @prefix }}
skip={{ [destination_field] }}
data={data}
resource={destination}
api={@api}
set_actor={@set_actor}
table={context[:data_layer][:table]}
prefix={@prefix}
skip={[destination_field]}
/>
</div>
"""
end
defp render_attributes(assigns, record, resource) do
~H"""
{{ {attributes, flags, bottom_attributes, _} =
~F"""
{{attributes, flags, bottom_attributes, _} =
AshAdmin.Components.Resource.Form.attributes(resource, :show)
nil }}
nil}
<div class="grid grid-cols-6 gap-6">
<div
:for={{ attribute <- attributes }}
class={{
:for={attribute <- attributes}
class={
"col-span-6",
"sm:col-span-2": short_text?(resource, attribute),
"sm:col-span-3": !long_text?(resource, attribute)
}}
}
>
<div class="block text-sm font-medium text-gray-700">{{ to_name(attribute.name) }}</div>
<div>{{ render_attribute(assigns, resource, record, attribute) }}</div>
<div class="block text-sm font-medium text-gray-700">{to_name(attribute.name)}</div>
<div>{render_attribute(assigns, resource, record, attribute)}</div>
</div>
</div>
<div :if={{ !Enum.empty?(flags) }} class="hidden sm:block" aria-hidden="true">
<div :if={!Enum.empty?(flags)} class="hidden sm:block" aria-hidden="true">
<div class="py-5">
<div class="border-t border-gray-200" />
</div>
</div>
<div class="grid grid-cols-6 gap-6" :if={{ !Enum.empty?(flags) }}>
<div class="grid grid-cols-6 gap-6" :if={!Enum.empty?(flags)}>
<div
:for={{ attribute <- flags }}
class={{
:for={attribute <- flags}
class={
"col-span-6",
"sm:col-span-2": short_text?(resource, attribute),
"sm:col-span-3": !long_text?(resource, attribute)
}}
}
>
<div class="block text-sm font-medium text-gray-700">{{ to_name(attribute.name) }}</div>
<div>{{ render_attribute(assigns, resource, record, attribute) }}</div>
<div class="block text-sm font-medium text-gray-700">{to_name(attribute.name)}</div>
<div>{render_attribute(assigns, resource, record, attribute)}</div>
</div>
</div>
<div :if={{ !Enum.empty?(bottom_attributes) }} class="hidden sm:block" aria-hidden="true">
<div :if={!Enum.empty?(bottom_attributes)} class="hidden sm:block" aria-hidden="true">
<div class="py-5">
<div class="border-t border-gray-200" />
</div>
</div>
<div class="grid grid-cols-6 gap-6" :if={{ !Enum.empty?(bottom_attributes) }}>
<div class="grid grid-cols-6 gap-6" :if={!Enum.empty?(bottom_attributes)}>
<div
:for={{ attribute <- bottom_attributes }}
class={{
:for={attribute <- bottom_attributes}
class={
"col-span-6",
"sm:col-span-2": short_text?(resource, attribute),
"sm:col-span-3": !(long_text?(resource, attribute) || Ash.Type.embedded_type?(attribute.type))
}}
}
>
<div class="block text-sm font-medium text-gray-700">{{ to_name(attribute.name) }}</div>
<div>{{ render_attribute(assigns, resource, record, attribute) }}</div>
<div class="block text-sm font-medium text-gray-700">{to_name(attribute.name)}</div>
<div>{render_attribute(assigns, resource, record, attribute)}</div>
</div>
</div>
"""
@ -256,24 +236,24 @@ defmodule AshAdmin.Components.Resource.Show do
all_classes = "mb-4 pb-4 shadow-md"
if Map.get(record, name) in [[], nil] do
~H"""
~F"""
None
"""
else
if nested? do
~H"""
~F"""
<ul>
<li :for={{ value <- List.wrap(Map.get(record, name)) }} class={{ all_classes }}>
{{ render_attribute(assigns, resource, Map.put(record, name, value), %{attribute | type: type}, true) }}
<li :for={value <- List.wrap(Map.get(record, name))} class={all_classes}>
{render_attribute(assigns, resource, Map.put(record, name, value), %{attribute | type: type}, true)}
</li>
</ul>
"""
else
~H"""
~F"""
<div class="shadow-md border mt-4 mb-4 ml-4">
<ul>
<li :for={{ value <- List.wrap(Map.get(record, name)) }} class={{ "my-4", all_classes }}>
{{ render_attribute(assigns, resource, Map.put(record, name, value), %{attribute | type: type}, true) }}
<li :for={value <- List.wrap(Map.get(record, name))} class={"my-4", all_classes}>
{render_attribute(assigns, resource, Map.put(record, name, value), %{attribute | type: type}, true)}
</li>
</ul>
</div>
@ -295,16 +275,16 @@ defmodule AshAdmin.Components.Resource.Show do
defp render_attribute(assigns, _resource, record, %{type: Ash.Type.Map} = attribute, _nested?) do
encoded = Jason.encode!(Map.get(record, attribute.name))
~H"""
~F"""
<div
phx-hook="JsonView"
data-json={{encoded}}
id={{"_#{AshAdmin.Helpers.encode_primary_key(record)}_#{attribute.name}_json"}}
data-json={encoded}
id={"_#{AshAdmin.Helpers.encode_primary_key(record)}_#{attribute.name}_json"}
/>
"""
rescue
_ ->
~H"""
~F"""
...
"""
end
@ -312,17 +292,17 @@ defmodule AshAdmin.Components.Resource.Show do
defp render_attribute(assigns, _resource, record, %{name: name, type: Ash.Type.Boolean}, _) do
case Map.get(record, name) do
true ->
~H"""
~F"""
<HeroIcon name="check" class="h-4 w-4 text-gray-600" />
"""
false ->
~H"""
~F"""
<HeroIcon name="x" class="h-4 w-4 text-gray-600" />
"""
nil ->
~H"""
~F"""
<HeroIcon name="minus" class="h-4 w-4 text-gray-600" />
"""
end
@ -333,20 +313,20 @@ defmodule AshAdmin.Components.Resource.Show do
both_classes = "ml-1 pl-2 pr-2"
if Map.get(record, attribute.name) in [nil, []] do
~H"""
~F"""
None
"""
else
if nested? do
~H"""
<div class={{ both_classes }}>
{{ render_attributes(assigns, Map.get(record, attribute.name), attribute.type) }}
~F"""
<div class={both_classes}>
{render_attributes(assigns, Map.get(record, attribute.name), attribute.type)}
</div>
"""
else
~H"""
<div class={{ "shadow-md border mt-4 mb-4 ml-2 rounded py-2 px-2", both_classes }}>
{{ render_attributes(assigns, Map.get(record, attribute.name), attribute.type) }}
~F"""
<div class={"shadow-md border mt-4 mb-4 ml-2 rounded py-2 px-2", both_classes}>
{render_attributes(assigns, Map.get(record, attribute.name), attribute.type)}
</div>
"""
end
@ -355,32 +335,32 @@ defmodule AshAdmin.Components.Resource.Show do
if attribute.type == Ash.Type.String do
cond do
short_text?(resource, attribute) ->
~H"""
{{ value!(Map.get(record, attribute.name)) }}
~F"""
{value!(Map.get(record, attribute.name))}
"""
long_text?(resource, attribute) ->
~H"""
~F"""
<textarea
rows="3"
cols="40"
disabled
class="resize-y mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
>{{ value!(Map.get(record, attribute.name)) }}</textarea>
>{value!(Map.get(record, attribute.name))}</textarea>
"""
true ->
~H"""
~F"""
<textarea
rows="1"
cols="20"
disabled
class="resize-y mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
>{{ value!(Map.get(record, attribute.name)) }}</textarea>
>{value!(Map.get(record, attribute.name))}</textarea>
"""
end
else
~H"{{ value!(Map.get(record, attribute.name)) }}"
~F"{value!(Map.get(record, attribute.name))}"
end
end
end

View file

@ -19,57 +19,44 @@ defmodule AshAdmin.Components.Resource.Table do
prop(format_fields, :any, default: [])
def render(assigns) do
~H"""
~F"""
<div>
<table class="rounded-t-lg m-5 w-5/6 mx-auto text-left">
<thead class="text-left border-b-2">
<th :for={{ attribute <- attributes(@resource, @attributes, @skip) }}>
{{ to_name(attribute.name) }}
<th :for={attribute <- attributes(@resource, @attributes, @skip)}>
{to_name(attribute.name)}
</th>
</thead>
<tbody>
<tr :for={{ record <- @data }} class="border-b-2">
<td :for={{ attribute <- attributes(@resource, @attributes, @skip) }} class="py-3">{{ render_attribute(@api, record, attribute, @format_fields) }}</td>
<td :if={{ @actions && actions?(@resource) }}>
<tr :for={record <- @data} class="border-b-2">
<td :for={attribute <- attributes(@resource, @attributes, @skip)} class="py-3">{render_attribute(@api, record, attribute, @format_fields)}</td>
<td :if={@actions && actions?(@resource)}>
<div class="flex h-max justify-items-center">
<div :if={{ AshAdmin.Resource.show_action(@resource) }}>
<LiveRedirect to={{ ash_show_path(@prefix, @api, @resource, record, @table) }}>
<div :if={AshAdmin.Resource.show_action(@resource)}>
<LiveRedirect to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(@resource)}&tab=show&table=#{@table}&primary_key=#{encode_primary_key(record)}"}>
<HeroIcon name="information-circle" class="h-5 w-5 text-gray-500" />
</LiveRedirect>
</div>
<div :if={{ Ash.Resource.Info.primary_action(@resource, :update) }}>
<LiveRedirect to={{ash_update_path(
@prefix,
@api,
@resource,
record,
Ash.Resource.Info.primary_action(@resource, :update).name,
@table
)}}>
<div :if={Ash.Resource.Info.primary_action(@resource, :update)}>
<LiveRedirect
to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(@resource)}&action_type=update&action=#{Ash.Resource.Info.primary_action(@resource, :update).name}&tab=update&table=#{@table}&primary_key=#{encode_primary_key(record)}"}>
<HeroIcon name="pencil" class="h-5 w-5 text-gray-500" />
</LiveRedirect>
</div>
<div :if={{ Ash.Resource.Info.primary_action(@resource, :destroy) }}>
<LiveRedirect to={{ash_destroy_path(
@prefix,
@api,
@resource,
record,
Ash.Resource.Info.primary_action(@resource, :destroy).name,
@table
)}}>
<div :if={Ash.Resource.Info.primary_action(@resource, :destroy)}>
<LiveRedirect to={"#{@prefix}?api=#{AshAdmin.Api.name(@api)}&resource=#{AshAdmin.Resource.name(@resource)}&action_type=destroy&action=#{Ash.Resource.Info.primary_action(@resource, :destroy).name}&tab=destroy&table=#{@table}&primary_key=#{encode_primary_key(record)}"}>
<HeroIcon name="x-circle" class="h-5 w-5 text-gray-500" />
</LiveRedirect>
</div>
<button
:if={{ AshAdmin.Resource.actor?(@resource) }}
:on-click={{ @set_actor }}
phx-value-resource={{ @resource }}
phx-value-api={{ @api }}
phx-value-pkey={{ encode_primary_key(record) }}
:if={AshAdmin.Resource.actor?(@resource)}
:on-click={@set_actor}
phx-value-resource={@resource}
phx-value-api={@api}
phx-value-pkey={encode_primary_key(record)}
>
<HeroIcon name="key" class="h-5 w-5 text-gray-500" />
</button>

View file

@ -1,7 +1,6 @@
defmodule AshAdmin.Components.TopNav do
@moduledoc false
use Surface.LiveComponent
import AshAdmin.Helpers
alias Surface.Components.LiveRedirect
alias AshAdmin.Components.TopNav.{ActorSelect, DrawerDropdown, TenantForm, Dropdown}
@ -24,14 +23,14 @@ defmodule AshAdmin.Components.TopNav do
prop(prefix, :any, required: true)
def render(assigns) do
~H"""
~F"""
<nav x-data="{ navOpen: false }" @keydown.window.escape="navOpen = false" class="bg-gray-800">
<div class="px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center w-full">
<div class="flex-shrink-0">
<h3 class="text-white text-lg">
<LiveRedirect to={{ ash_admin_path(@prefix) }}>
<LiveRedirect to={@prefix}>
Admin
</LiveRedirect>
</h3>
@ -40,34 +39,34 @@ defmodule AshAdmin.Components.TopNav do
<div class="flex justify-between">
<div class="ml-10 flex items-center">
<Dropdown
:for={{ api <- @apis }}
active={{ api == @api }}
:for={api <- @apis}
active={api == @api}
class="mr-1"
id={{ AshAdmin.Api.name(api) <> "_api_nav" }}
name={{ AshAdmin.Api.name(api) }}
groups={{ dropdown_groups(@prefix, @resource, api) }}
id={AshAdmin.Api.name(api) <> "_api_nav"}
name={AshAdmin.Api.name(api)}
groups={dropdown_groups(@prefix, @resource, api)}
/>
</div>
<div class="ml-10 flex items-center">
<ActorSelect
:if={{ @actor_resources != []}}
actor_resources={{ @actor_resources }}
authorizing={{ @authorizing }}
actor_paused={{ @actor_paused }}
actor={{ @actor }}
toggle_authorizing={{ @toggle_authorizing }}
toggle_actor_paused={{ @toggle_actor_paused }}
clear_actor={{ @clear_actor }}
actor_api={{ @actor_api }}
api={{ @api }}
prefix={{ @prefix }}
:if={@actor_resources != []}
actor_resources={@actor_resources}
authorizing={@authorizing}
actor_paused={@actor_paused}
actor={@actor}
toggle_authorizing={@toggle_authorizing}
toggle_actor_paused={@toggle_actor_paused}
clear_actor={@clear_actor}
actor_api={@actor_api}
api={@api}
prefix={@prefix}
/>
<TenantForm
:if={{ show_tenant_form?(@apis) }}
tenant={{ @tenant }}
:if={show_tenant_form?(@apis)}
tenant={@tenant}
id="tenant_editor"
set_tenant={{ @set_tenant }}
clear_tenant={{ @clear_tenant }}
set_tenant={@set_tenant}
clear_tenant={@clear_tenant}
/>
</div>
</div>
@ -104,33 +103,33 @@ defmodule AshAdmin.Components.TopNav do
<div class="relative px-2 pt-2 pb-3 sm:px-3">
<div class="block px-4 py-2 text-sm">
<ActorSelect
:if={{ @actor_resources != [] }}
actor_resources={{ @actor_resources }}
authorizing={{ @authorizing }}
actor_paused={{ @actor_paused }}
actor={{ @actor }}
toggle_authorizing={{ @toggle_authorizing }}
toggle_actor_paused={{ @toggle_actor_paused }}
clear_actor={{ @clear_actor }}
actor_api={{ @actor_api }}
api={{ @api }}
prefix={{ @prefix }}
:if={@actor_resources != []}
actor_resources={@actor_resources}
authorizing={@authorizing}
actor_paused={@actor_paused}
actor={@actor}
toggle_authorizing={@toggle_authorizing}
toggle_actor_paused={@toggle_actor_paused}
clear_actor={@clear_actor}
actor_api={@actor_api}
api={@api}
prefix={@prefix}
/>
</div>
<div class="block px-4 py-2 text-sm">
<TenantForm
:if={{ show_tenant_form?(@apis) }}
tenant={{ @tenant }}
:if={show_tenant_form?(@apis)}
tenant={@tenant}
id="tenant_editor_drawer"
set_tenant={{ @set_tenant }}
clear_tenant={{ @clear_tenant }}
set_tenant={@set_tenant}
clear_tenant={@clear_tenant}
/>
</div>
<DrawerDropdown
:for={{ api <- @apis }}
id={{ AshAdmin.Api.name(api) <> "_api_nav_drawer" }}
name={{ AshAdmin.Api.name(api) }}
groups={{ dropdown_groups(@prefix, @resource, api) }}
:for={api <- @apis}
id={AshAdmin.Api.name(api) <> "_api_nav_drawer"}
name={AshAdmin.Api.name(api)}
groups={dropdown_groups(@prefix, @resource, api)}
/>
</div>
</div>
@ -143,7 +142,8 @@ defmodule AshAdmin.Components.TopNav do
for resource <- Ash.Api.resources(api) do
%{
text: AshAdmin.Resource.name(resource),
to: ash_admin_path(prefix, api, resource),
to:
"#{prefix}?api=#{AshAdmin.Api.name(api)}&resource=#{AshAdmin.Resource.name(resource)}",
active: resource == current_resource
}
end

View file

@ -18,13 +18,13 @@ defmodule AshAdmin.Components.TopNav.ActorSelect do
prop(prefix, :any, required: true)
def render(assigns) do
~H"""
~F"""
<div id="actor-hook" class="flex items-center mr-5 text-white" phx-hook="Actor">
<div>
<span>
<button :on-click={{ @toggle_authorizing }} type="button">
<button :on-click={@toggle_authorizing} type="button">
<svg
:if={{ @authorizing }}
:if={@authorizing}
width="1em"
height="1em"
viewBox="0 0 16 16"
@ -37,7 +37,7 @@ defmodule AshAdmin.Components.TopNav.ActorSelect do
/>
</svg>
<svg
:if={{ !@authorizing }}
:if={!@authorizing}
width="1em"
height="1em"
viewBox="0 0 16 16"
@ -50,9 +50,9 @@ defmodule AshAdmin.Components.TopNav.ActorSelect do
/>
</svg>
</button>
<button :if={{@actor}} :on-click={{ @toggle_actor_paused }} type="button">
<button :if={@actor} :on-click={@toggle_actor_paused} type="button">
<svg
:if={{ @actor_paused }}
:if={@actor_paused}
width="1em"
height="1em"
viewBox="0 0 16 16"
@ -62,7 +62,7 @@ defmodule AshAdmin.Components.TopNav.ActorSelect do
<path d="M11.596 8.697l-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z" />
</svg>
<svg
:if={{ !@actor_paused }}
:if={!@actor_paused}
width="1em"
height="1em"
viewBox="0 0 16 16"
@ -73,19 +73,12 @@ defmodule AshAdmin.Components.TopNav.ActorSelect do
</svg>
</button>
<LiveRedirect
:if={{@actor}}
:if={@actor}
class="hover:text-blue-400 hover:underline"
to={{ash_show_path(
@prefix,
@actor_api,
@actor.__struct__,
@actor,
nil
)}}
>
{{ user_display(@actor) }}
to={"#{@prefix}?api=#{AshAdmin.Api.name(@actor_api)}&resource=#{AshAdmin.Resource.name(@actor.__struct__)}&tab=show&primary_key=#{encode_primary_key(@actor)}"}>
{user_display(@actor)}
</LiveRedirect>
<button :if={{@actor}} :on-click={{ @clear_actor }} type="button">
<button :if={@actor} :on-click={@clear_actor} type="button">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="white" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
@ -99,44 +92,30 @@ defmodule AshAdmin.Components.TopNav.ActorSelect do
</button>
</span>
</div>
<div :if={{ !@actor }}>
{{ render_actor_link(assigns, @actor_resources) }}
<div :if={!@actor}>
{render_actor_link(assigns, @actor_resources)}
</div>
</div>
"""
end
defp render_actor_link(assigns, [{api, resource}]) do
~H"""
<LiveRedirect to={{ash_action_path(
@prefix,
api,
resource,
:read,
Ash.Resource.Info.primary_action(resource, :read).name,
nil
)}}>
Set {{ AshAdmin.Resource.name(resource) }}
~F"""
<LiveRedirect to={"#{@prefix}?api=#{AshAdmin.Api.name(api)}&resource=#{AshAdmin.Resource.name(resource)}&action_type=read"}>
Set {AshAdmin.Resource.name(resource)}
</LiveRedirect>
"""
end
defp render_actor_link(assigns, apis_and_resources) do
~H"""
~F"""
<div aria-labelledby="actor-banner">
<LiveRedirect
to={{ash_action_path(
@prefix,
api,
resource,
:read,
Ash.Resource.Info.primary_action(resource, :read).name,
nil
)}}
:for.with_index={{ {{api, resource}, i} <- apis_and_resources }}
to={"#{@prefix}?api=#{AshAdmin.Api.name(api)}&resource=#{AshAdmin.Resource.name(resource)}&action_type=read"}
:for.with_index={{{api, resource}, i} <- apis_and_resources}
>
Set {{ AshAdmin.Resource.name(resource) }}
<span :if={{ i != Enum.count(apis_and_resources) - 1 }}>
Set {AshAdmin.Resource.name(resource)}
<span :if={i != Enum.count(apis_and_resources) - 1}>
|
</span>
</LiveRedirect>

View file

@ -9,22 +9,22 @@ defmodule AshAdmin.Components.TopNav.DrawerDropdown do
prop(id, :string, required: true)
def render(assigns) do
~H"""
~F"""
<div class="relative">
<div x-data="{isOpen: false}">
<a
@click="isOpen = !isOpen"
id={{ "#{@id}_dropdown_drawer" }}
id={"#{@id}_dropdown_drawer"}
class="mt-1 block px-3 py-2 rounded-t text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700"
href="#"
x-bind:class="{'text-white bg-gray-700': isOpen}"
>
{{ @name }}
{@name}
</a>
<div
:for={{ group <- @groups }}
aria-labelledby={{ "#{@id}_dropown_drawer" }}
:for={group <- @groups}
aria-labelledby={"#{@id}_dropown_drawer"}
class="bg-gray-700 text-white"
x-show="isOpen"
role="menu"
@ -36,12 +36,12 @@ defmodule AshAdmin.Components.TopNav.DrawerDropdown do
x-transition:leave-end="opacity-0 transform -translate-y-3"
>
<LiveRedirect
:for={{ link <- group }}
to={{ link.to }}
:for={link <- group}
to={link.to}
class="block px-4 py-2 text-sm hover:bg-gray-200 hover:text-gray-900"
opts={{ role: "menuitem" }}
opts={role: "menuitem"}
>
{{ link.text }}
{link.text}
</LiveRedirect>
</div>
</div>

View file

@ -11,22 +11,22 @@ defmodule AshAdmin.Components.TopNav.Dropdown do
prop(class, :css_class)
def render(assigns) do
~H"""
<div class={{ "relative", @class }}>
~F"""
<div class={"relative", @class}>
<div x-data="{isOpen: false}">
<button
type="button"
class={{
class={
"inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500",
"bg-gray-800 hover:bg-gray-900 text-white": @active,
"bg-white text-gray-700 hover:bg-gray-300": !@active
}}
}
@click="isOpen = !isOpen"
id={{ "#{@id}_dropown" }}
id={"#{@id}_dropown"}
aria-haspopup="true"
aria-expanded="true"
>
{{ @name }}
{@name}
<svg
class="-mr-1 ml-2 h-5 w-5"
@ -45,10 +45,10 @@ defmodule AshAdmin.Components.TopNav.Dropdown do
<div
x-show="isOpen"
class={{
class={
"origin-top-right absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 z-10",
"bg-gray-600 hover:bg-gray-700": single_active_group?(@groups)
}}
}
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-0 scale-95"
@ -58,26 +58,26 @@ defmodule AshAdmin.Components.TopNav.Dropdown do
role="menu"
aria-orientation="vertical"
@click.away="isOpen=false"
id={{ "#{@id}_dropown" }}
id={"#{@id}_dropown"}
>
<div
:for={{ group <- @groups }}
:for={group <- @groups}
class="py-1"
role="menu"
aria-orientation="vertical"
aria-labelledby={{ "#{@id}_dropown" }}
aria-labelledby={"#{@id}_dropown"}
>
<LiveRedirect
:for={{ link <- group }}
to={{ link.to }}
class={{
:for={link <- group}
to={link.to}
class={
"block px-4 py-2 text-sm ",
"bg-gray-600 text-white hover:bg-gray-700": Map.get(link, :active),
"text-gray-700 hover:bg-gray-100 hover:text-gray-900": !Map.get(link, :active)
}}
opts={{ role: "menuitem" }}
}
opts={role: "menuitem"}
>
{{ link.text }}
{link.text}
</LiveRedirect>
</div>
</div>

View file

@ -9,10 +9,10 @@ defmodule AshAdmin.Components.TopNav.TenantForm do
prop(set_tenant, :event, required: true)
def render(assigns) do
~H"""
~F"""
<div id="tenant-hook" class="relative text-white" phx-hook="Tenant">
<form :if={{ @editing_tenant }} :on-submit={{ @set_tenant }}>
<input type="text" name="tenant" value={{ @tenant }} class={{ "text-black": @editing_tenant }}>
<form :if={@editing_tenant} :on-submit={@set_tenant}>
<input type="text" name="tenant" value={@tenant} class={"text-black": @editing_tenant}>
<button :on-click="stop_editing_tenant">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="white" xmlns="http://www.w3.org/2000/svg">
<path
@ -26,7 +26,7 @@ defmodule AshAdmin.Components.TopNav.TenantForm do
</svg>
</button>
</form>
<button :if={{ @tenant }} :on-click={{ @clear_tenant }}>
<button :if={@tenant} :on-click={@clear_tenant}>
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="white" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
@ -34,8 +34,8 @@ defmodule AshAdmin.Components.TopNav.TenantForm do
/>
</svg>
</button>
<a :if={{ !@editing_tenant }} href="#" :on-click="start_editing_tenant">
{{ @tenant || "No tenant" }}
<a :if={!@editing_tenant} href="#" :on-click="start_editing_tenant">
{@tenant || "No tenant"}
</a>
</div>
"""

View file

@ -48,138 +48,6 @@ defmodule AshAdmin.Helpers do
end
end
defp prefix(nil, path), do: path
defp prefix(prefix, path), do: prefix <> path
def ash_admin_path(prefix) do
prefix(prefix, "/")
end
def ash_admin_path(prefix, api) do
prefix(prefix, "/#{AshAdmin.Api.name(api)}")
end
def ash_admin_path(prefix, api, resource) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}"
)
end
def ash_create_path(prefix, api, resource) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/create"
)
end
def ash_create_path(prefix, api, resource, action_name, nil) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/create/#{action_name}"
)
end
def ash_create_path(prefix, api, resource, action_name, table) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/#{table}/create/#{
action_name
}"
)
end
def ash_update_path(prefix, api, resource, record) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/update/#{
encode_primary_key(record)
}"
)
end
def ash_update_path(prefix, api, resource, record, action_name, nil) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/update/#{action_name}/#{
encode_primary_key(record)
}"
)
end
def ash_update_path(prefix, api, resource, record, action_name, table) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/#{table}/update/#{
action_name
}/#{encode_primary_key(record)}"
)
end
def ash_destroy_path(prefix, api, resource, record) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/destroy/#{
encode_primary_key(record)
}"
)
end
def ash_destroy_path(prefix, api, resource, record, action_name, nil) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/destroy/#{action_name}/#{
encode_primary_key(record)
}"
)
end
def ash_destroy_path(prefix, api, resource, record, action_name, table) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/#{table}/destroy/#{
action_name
}#{encode_primary_key(record)}"
)
end
def ash_action_path(prefix, api, resource, action_type, action_name, nil) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/#{action_type}/#{
action_name
}"
)
end
def ash_action_path(prefix, api, resource, action_type, action_name, table) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/#{table}/#{action_type}/#{
action_name
}"
)
end
# sobelow_skip ["DOS.StringToAtom"]
def ash_show_path(prefix, api, resource, record, nil) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/show/#{
encode_primary_key(record)
}"
)
end
def ash_show_path(prefix, api, resource, record, table) do
prefix(
prefix,
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}/#{table}/show/#{
encode_primary_key(record)
}"
)
end
def encode_primary_key(record) do
pkey = Ash.Resource.Info.primary_key(record.__struct__)

View file

@ -21,16 +21,11 @@ defmodule AshAdmin.PageLive do
def mount(
_params,
%{
"prefix" => prefix,
"api" => api,
"apis" => apis,
"tab" => tab,
"action_type" => action_type,
"action_name" => action_name,
"resource" => resource
"prefix" => prefix
} = session,
socket
) do
otp_app = socket.endpoint.config(:otp_app)
socket = assign(socket, :prefix, prefix)
actor_paused =
@ -40,79 +35,66 @@ defmodule AshAdmin.PageLive do
AshAdmin.ActorPlug.session_bool(session["actor_paused"])
end
action =
if action_type && action_name && resource do
Ash.Resource.Info.action(resource, action_name, action_type)
end
tables =
if resource do
AshAdmin.Resource.polymorphic_tables(resource, apis)
end
apis = apis(otp_app)
{:ok,
socket
|> Surface.init()
|> assign(:prefix, prefix)
|> assign(:api, api)
|> assign(:apis, apis)
|> assign(:resource, resource)
|> assign(:action, action)
|> assign(:primary_key, nil)
|> assign(:record, nil)
|> assign(:tab, tab)
|> assign(:actor_resources, actor_resources(apis))
|> assign(:apis, apis)
|> assign(:tenant, session["tenant"])
|> assign(:actor, AshAdmin.ActorPlug.actor_from_session(session))
|> assign(:actor_api, AshAdmin.ActorPlug.actor_api_from_session(session))
|> assign(:actor, AshAdmin.ActorPlug.actor_from_session(socket.endpoint, session))
|> assign(:actor_api, AshAdmin.ActorPlug.actor_api_from_session(socket.endpoint, session))
|> assign(:actor_resources, actor_resources(apis))
|> assign(
:authorizing,
AshAdmin.ActorPlug.session_bool(session["actor_authorizing"]) || false
)
|> assign(:actor_paused, actor_paused)
|> assign(:tables, tables)
|> assign(:table, session["table"] || Enum.at(tables || [], 0))}
|> assign(:actor_paused, actor_paused)}
end
@impl true
def render(assigns) do
~H"""
~F"""
<TopNav
id="top_nav"
apis={{ @apis }}
api={{ @api }}
actor_api={{ @actor_api }}
resource={{ @resource }}
tenant={{ @tenant }}
actor_resources={{ @actor_resources }}
authorizing={{ @authorizing }}
actor_paused={{ @actor_paused }}
actor={{ @actor }}
apis={@apis}
api={@api}
actor_api={@actor_api}
resource={@resource}
tenant={@tenant}
actor_resources={@actor_resources}
authorizing={@authorizing}
actor_paused={@actor_paused}
actor={@actor}
set_tenant="set_tenant"
clear_tenant="clear_tenant"
toggle_authorizing="toggle_authorizing"
toggle_actor_paused="toggle_actor_paused"
clear_actor="clear_actor"
prefix={{ @prefix }}
prefix={@prefix}
/>
<Resource
:if={{ @resource }}
id={{ @resource }}
resource={{ @resource }}
:if={@resource}
id={@resource}
resource={@resource}
set_actor="set_actor"
primary_key={{ @primary_key }}
record={{ @record }}
api={{ @api }}
tab={{ @tab }}
url_path={{ @url_path }}
params={{ @params }}
action={{ @action }}
tenant={{ @tenant }}
actor={{ unless @actor_paused, do: @actor }}
authorizing={{ @authorizing }}
table={{ @table }}
tables={{ @tables }}
prefix={{ @prefix }}
primary_key={@primary_key}
record={@record}
api={@api}
tab={@tab}
action_type={@action_type}
url_path={@url_path}
params={@params}
action={@action}
tenant={@tenant}
actor={unless @actor_paused, do: @actor}
authorizing={@authorizing}
table={@table}
tables={@tables}
prefix={@prefix}
/>
"""
end
@ -128,13 +110,206 @@ defmodule AshAdmin.PageLive do
end)
end
defp apis(otp_app) do
otp_app
|> Application.get_env(:ash_apis)
|> Enum.filter(&AshAdmin.Api.show?/1)
end
# defp find_api(api, otp_app) do
# otp_app
# |> apis()
# |> Enum.find(fn api ->
# AshAdmin.Api.name(api) == api
# end)
# end
# defp find_resource(api, resource, otp_app) do
# case find_api(api, otp_app) do
# nil ->
# nil
# api ->
# api
# |> Ash.Api.resources()
# |> Enum.find(fn resource ->
# AshAdmin.Resource.name(resource) == resource
# end)
# end
# end
# defp match_result(%{path: path}, otp_app) when path == "/" || path == "" do
# {:ok, [api: Enum.at(apis(otp_app), 0)]}
# end
# defp match_result(url, otp_app) do
# url.path
# |> Enum.split("/")
# |> case do
# [api] ->
# case find_api(api, otp_app) do
# nil ->
# :error
# api ->
# {:ok, api: api, tab: "info"}
# end
# [api, resource] ->
# case find_resource(api, resource, otp_app) do
# {api, resource} ->
# {:ok, api: api, resource: resource, tab: "info"}
# nil ->
# :error
# end
# [api, resource, table_or_action_type] ->
# case find_table_or_action_type(api, resource, table_or_action_type, otp_app) do
# {:table, api, resource} ->
# {:ok, api: api, resource: resource, table: table_or_action_type}
# {:action, api, resource, action_type} ->
# action = Ash.Resource.Info.primary_action!(resource, action_type)
# {:ok, api: api, resource: resource, action_type: action_type, action: action.name}
# end
# [api, resource, table_or_action_type, action_type_or_action_name, otp_app] ->
# case find_action_type_or_action_name(
# api,
# resource,
# table_or_action_type,
# action_type_or_action_name
# ) do
# {:action_name, api, resource, action_type, action_name} ->
# {:ok, api: api, resource: resource, action_type: action_type, action: action_name}
# {:action_type, api, resource, action_type} ->
# action = Ash.Resource.Info.primary_action!(resource, action_type)
# {:ok,
# api: api,
# resource: resource,
# action_type: action_type,
# action: action.name,
# table: table_or_action_type}
# {:action_type_with_pkey, api, resource, action_type, primary_key} ->
# action = Ash.Resource.Info.primary_action!(resource, action_type)
# {:ok, api: api, resource: resource, action_type: action_type, action: action.name}
# end
# [api, resource, table_or_action_type, action_type_or_action_name, primary_key] ->
# case find_action_type_or_action_name_with_primary_key(
# api,
# resource,
# table_or_action_type,
# action_type_or_action_name,
# otp_app
# ) do
# {:table, api, resource, action_type, primary_key} ->
# action = Ash.Resource.Info.primary_action!(resource, action_type)
# {:ok,
# api: api,
# resource: resource,
# action_type: action_type,
# primary_key: primary_key,
# table: table_or_action_type,
# action: action.name}
# {:action_name, api, resource, action_type, action_name, primary_key} ->
# {:ok,
# api: api,
# resource: resource,
# action_type: action_type,
# action: action_name,
# primary_key: primary_key}
# end
# [api, resource, table, action_type, action_name, primary_key] ->
# action_type = case action_type do
# "update" -> :update
# "destroy" -> :destroy
# end
# case find_resource(api, resource, otp_app) do
# end
# end
# end
defp assign_api(socket, api) do
api =
Enum.find(socket.assigns.apis, fn shown_api ->
AshAdmin.Api.name(shown_api) == api
end) || Enum.at(socket.assigns.apis, 0)
assign(socket, :api, api)
end
defp assign_resource(socket, resource) do
resources = Ash.Api.resources(socket.assigns.api)
resource =
Enum.find(resources, fn api_resource ->
AshAdmin.Resource.name(api_resource) == resource
end) || Enum.at(resources, 0)
assign(socket, :resource, resource)
end
defp assign_action(socket, action, action_type) do
action_type =
case action_type do
"read" -> :read
"update" -> :update
"create" -> :create
"destroy" -> :destroy
nil -> nil
end
if action_type do
action =
Enum.find(Ash.Resource.Info.actions(socket.assigns.resource), fn resource_action ->
to_string(resource_action.name) == action && resource_action.type == action_type
end) || Ash.Resource.Info.primary_action!(socket.assigns.resource, action_type)
assign(socket, action_type: action_type, action: action)
else
assign(socket, action: nil, action_type: nil)
end
end
defp assign_tables(socket, table) do
tables =
if socket.assigns.resource do
AshAdmin.Resource.polymorphic_tables(socket.assigns.resource, socket.assigns.apis)
else
[]
end
if table && table != "" do
assign(socket, table: table, tables: tables)
else
assign(socket, table: Enum.at(tables, 0), tables: tables)
end
end
@impl true
def handle_params(params, url, socket) do
url = URI.parse(url)
socket =
if params["primary_key"] do
case decode_primary_key(socket.assigns.resource, params["primary_key"]) do
socket
|> assign_api(params["api"])
|> assign_resource(params["resource"])
|> assign_action(params["action"], params["action_type"])
|> assign_tables(params["table"])
|> assign(primary_key: params["primary_key"], tab: params["tab"])
socket =
if socket.assigns[:primary_key] do
case decode_primary_key(socket.assigns.resource, socket.assigns[:primary_key]) do
{:ok, primary_key} ->
actor =
if socket.assigns.actor_paused do
@ -157,9 +332,7 @@ defmodule AshAdmin.PageLive do
case record do
{:error, error} ->
Logger.warn(
"Error while loading record on admin dashboard\n: #{
Exception.format(:error, error)
}"
"Error while loading record on admin dashboard\n: #{Exception.format(:error, error)}"
)
{:ok, _} ->
@ -185,6 +358,10 @@ defmodule AshAdmin.PageLive do
socket
|> assign(:url_path, url.path)
|> assign(:params, params)}
# :error ->
# {:error, "Not Found"}
# end
end
defp to_one_relationships(resource) do
@ -238,15 +415,18 @@ defmodule AshAdmin.PageLive do
|> Ash.Query.filter(^pkey_filter)
|> api.read_one!(action: action)
api_name = AshAdmin.Api.name(api)
resource_name = AshAdmin.Resource.name(resource)
{:noreply,
socket
|> push_event(
"set_actor",
%{
resource: to_string(resource),
resource: to_string(resource_name),
primary_key: encode_primary_key(actor),
action: to_string(action.name),
api: to_string(api)
api: to_string(api_name)
}
)
|> assign(actor: actor, actor_api: api)}

View file

@ -44,8 +44,6 @@ defmodule AshAdmin.Router do
Defines an AshAdmin route.
It expects the `path` the admin dashboard will be mounted at
and a set of options.
## Options
* `:apis` - The list of Apis to include in the admin dashboard
## Examples
defmodule MyAppWeb.Router do
use Phoenix.Router
@ -63,260 +61,23 @@ defmodule AshAdmin.Router do
end
"""
defmacro ash_admin(path, opts \\ []) do
quote bind_quoted: binding() do
scope path, alias: false, as: false do
import Phoenix.LiveView.Router, only: [live: 4]
prefix =
if opts[:prefix] do
opts[:prefix] <> path
else
path
end
prefix = String.trim_trailing(prefix, "/")
apis = opts[:apis]
Enum.each(apis, &Code.ensure_compiled/1)
api = List.first(opts[:apis])
resource =
api
|> Ash.Api.resources()
|> List.first()
quote bind_quoted: [path: path, opts: opts] do
import Phoenix.LiveView.Router
live_socket_path = Keyword.get(opts, :live_socket_path, "/live")
live_session :default,
session: {AshAdmin.Router, :__session__, %{"prefix" => path}},
root_layout: {AshAdmin.LayoutView, :admin} do
live(
"/",
"#{path}/*route",
AshAdmin.PageLive,
:page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"tab" => nil,
"resource" => nil,
"action_type" => nil,
"action_name" => nil
})
private: %{live_socket_path: live_socket_path}
)
for api <- apis do
live(
"/#{AshAdmin.Api.name(api)}",
AshAdmin.PageLive,
:api_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"tab" => "info",
"resource" => nil,
"action_type" => nil,
"action_name" => nil
})
)
for resource <- Ash.Api.resources(api) do
for {table, alias_part, polymorphic_part} <-
AshAdmin.Router.polymorphic_parts(resource, apis) do
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{polymorphic_part}",
AshAdmin.PageLive,
:resource_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"tab" => "info",
"resource" => resource,
"action_type" => nil,
"action_name" => nil,
"table" => table
})
)
if Enum.any?(Ash.Resource.Info.actions(resource), &(&1.type == :create)) do
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{
polymorphic_part
}/create",
AshAdmin.PageLive,
:resource_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"resource" => resource,
"tab" => "create",
"action_type" => :create,
"action_name" => Ash.Resource.Info.primary_action!(resource, :create).name,
"table" => table
})
)
end
for %{type: :create} = action <- Ash.Resource.Info.actions(resource) do
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{
polymorphic_part
}/create/#{action.name}",
AshAdmin.PageLive,
:resource_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"resource" => resource,
"tab" => "create",
"action_type" => :create,
"action_name" => action.name,
"table" => table
})
)
end
if Enum.any?(Ash.Resource.Info.actions(resource), &(&1.type == :update)) do
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{
polymorphic_part
}/update/:primary_key",
AshAdmin.PageLive,
:resource_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"resource" => resource,
"tab" => "update",
"action_type" => :update,
"action_name" => Ash.Resource.Info.primary_action!(resource, :update).name,
"table" => table
})
)
for %{type: :update} = action <- Ash.Resource.Info.actions(resource) do
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{
polymorphic_part
}/update/#{action.name}/:primary_key",
AshAdmin.PageLive,
:resource_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"resource" => resource,
"tab" => "update",
"action_type" => :update,
"action_name" => action.name,
"table" => table
})
)
end
end
if Enum.any?(Ash.Resource.Info.actions(resource), &(&1.type == :destroy)) do
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{
polymorphic_part
}/destroy/:primary_key",
AshAdmin.PageLive,
:resource_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"resource" => resource,
"tab" => "destroy",
"action_type" => :destroy,
"action_name" => Ash.Resource.Info.primary_action!(resource, :destroy).name,
"table" => table
})
)
for %{type: :destroy} = action <- Ash.Resource.Info.actions(resource) do
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{
polymorphic_part
}/destroy/#{action.name}/:primary_key",
AshAdmin.PageLive,
:resource_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"resource" => resource,
"tab" => "destroy",
"action_type" => :destroy,
"action_name" => action.name,
"table" => table
})
)
end
end
show_action = AshAdmin.Resource.show_action(resource)
if show_action do
action =
Ash.Resource.Info.action(resource, AshAdmin.Resource.show_action(resource))
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{
polymorphic_part
}/show/:primary_key",
AshAdmin.PageLive,
:show_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"resource" => resource,
"tab" => "read",
"action_type" => :read,
"action_name" => action.name,
"table" => table
})
)
end
for %{type: :read} = action <- Ash.Resource.Info.actions(resource) do
live(
"/#{AshAdmin.Api.name(api)}/#{AshAdmin.Resource.name(resource)}#{
polymorphic_part
}/#{action.type}/#{action.name}",
AshAdmin.PageLive,
:resource_page,
AshAdmin.Router.__options__(opts, %{
"prefix" => prefix,
"apis" => apis,
"api" => api,
"resource" => resource,
"tab" => "data",
"action_type" => :read,
"action_name" => action.name,
"table" => table
})
)
end
end
end
end
end
end
end
@doc false
def __options__(options, session) do
live_socket_path = Keyword.get(options, :live_socket_path, "/live")
[
session: {__MODULE__, :__session__, [session]},
private: %{live_socket_path: live_socket_path},
layout: {AshAdmin.LayoutView, :admin}
]
end
@cookies_to_replicate [
"tenant",
"actor_resource",
@ -339,15 +100,4 @@ defmodule AshAdmin.Router do
end
end)
end
@doc false
def polymorphic_parts(resource, apis) do
case AshAdmin.Resource.polymorphic_tables(resource, apis) do
[] ->
[{nil, "", ""}]
tables ->
[{nil, "", ""} | Enum.map(tables, &{&1, &1, "/#{&1}"})]
end
end
end

10
mix.exs
View file

@ -88,14 +88,14 @@ defmodule AshAdmin.MixProject do
[
{:ash, "~> 1.47 and >= 1.47.3"},
# {:ash, path: "../ash", override: true},
{:ash_phoenix, "~> 0.5 and >= 0.5.7"},
{:ash_phoenix, "~> 0.5 and >= 0.5.11"},
# {:ash_phoenix, path: "../ash_phoenix"},
{:surface, "~> 0.4.1"},
{:phoenix_live_view, "~> 0.15.4"},
{:phoenix_html, "~> 2.14.1 or ~> 2.15"},
{:surface, "~> 0.5.1"},
{:phoenix_live_view, "~> 0.16"},
{:phoenix_html, "~> 3.0"},
{:jason, "~> 1.0"},
# Dev dependencies
{:surface_formatter, "~> 0.4.1", only: [:dev, :test]},
{:surface_formatter, "~> 0.5.0", only: [:dev, :test]},
{:plug_cowboy, "~> 2.0", only: [:dev, :test]},
{:phoenix_live_reload, "~> 1.2", only: [:dev, :test]},
{:ash_postgres, "~> 0.40.9", only: [:dev, :test]},

View file

@ -1,6 +1,6 @@
%{
"ash": {:hex, :ash, "1.47.3", "6c00875d4c95e859f67073d032069b7ec3019e507d57d8d1ea8c8ecf79d1cec9", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:timex, ">= 3.0.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "0aa1b7ccf699624179a36d0603376bb4782ff032a7fc4a6b6671341990142e07"},
"ash_phoenix": {:hex, :ash_phoenix, "0.5.7", "0b11395376b135c9daa30b12b826428ca585003ad54255e05a57c2bb19c0f839", [:mix], [{:ash, "~> 1.46 and >= 1.46.8", [hex: :ash, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "e10e43fba8d467c9f13db7b5541ef0ac935111a688d628dc264655c3419edd5a"},
"ash": {:hex, :ash, "1.47.11", "1ce1f77cb0f687a01880b9573102f516be80c90880dcbe4cb53746e5ed765df9", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:timex, ">= 3.0.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "02b73a3fb80fd90c54301c69c5713e4422a538da0b2f76a1d0ab3cdadc6d94a2"},
"ash_phoenix": {:hex, :ash_phoenix, "0.5.11", "8c3ce8a5d79803b6e87bf2663e58d10d779fc27b90c00d71c3e8e713dedc3b3b", [:mix], [{:ash, "~> 1.47 and >= 1.47.8", [hex: :ash, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "e7b6a2eac05105e56a34617a54eae7cc40cdfe7c12bb6166296b473be6389236"},
"ash_policy_authorizer": {:hex, :ash_policy_authorizer, "0.16.2", "9d8446bd7d79ac2c77b459ca0e79cd4a3873eed57d6853aebf7bf01002d32693", [:mix], [{:ash, "~> 1.46", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "508a0c17f906d5f21ef461026957258a3ba90d1ebd2e9ceb480e88f84e613e08"},
"ash_postgres": {:hex, :ash_postgres, "0.40.9", "b184e6ddc200271157ff84bfadd5e2a212f363cf0cdcff15fe7d9b8d10f012ff", [:mix], [{:ash, "~> 1.46 and >= 1.46.11", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.5", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "6b2e62b1e47f0898c5f576ed68c3fe92feb59d69d4dfefcf51e3581afb72fd18"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
@ -37,28 +37,28 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mime": {:hex, :mime, "2.0.1", "0de4c81303fe07806ebc2494d5321ce8fb4df106e34dd5f9d787b637ebadc256", [:mix], [], "hexpm", "7a86b920d2aedce5fb6280ac8261ac1a739ae6c1a1ad38f5eadf910063008942"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_options": {:hex, :nimble_options, "0.3.6", "365d03c05d43483d3eacf820671dafce5b49d692667b3bb8cae28447fd2414ef", [:mix], [], "hexpm", "1c1d3536c4aee1be2c8f3c691bf27c62dbd88d9bb3a0b1a011913453932e8c15"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.5.9", "a6368d36cfd59d917b37c44386e01315bc89f7609a10a45a22f47c007edf2597", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7e4bce20a67c012f1fbb0af90e5da49fa7bf0d34e3a067795703b74aef75427d"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"phoenix": {:hex, :phoenix, "1.5.12", "75fddb14c720388eea93d33886166a690416a7ff8633fbd93f364355b6fe1166", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f0ae6734fcc18bbaa646c161e2febc46fb899eae43f82679b92530983324113"},
"phoenix_html": {:hex, :phoenix_html, "3.0.2", "0d71bd7dfa5fad2103142206e25e16accd64f41bcbd0002af3f0da17e530968d", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d6c6e85d9bef8d52a5a66fcccd15529651f379eaccbf10500343a17f6f814f82"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.15.7", "09720b8e5151b3ca8ef739cd7626d4feb987c69ba0b509c9bbdb861d5a365881", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a756cf662420272d0f1b3b908cce5222163b5a95aa9bab404f9d29aff53276e"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.16.1", "a17652e936718b6b6b52ef64d4b9860bc30c41b9a491e25f2b49a70604efa436", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "94bbc572471ad151b756b38dd10acbf91e0bcc132ad8b78240baa0dcf77cea74"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"picosat_elixir": {:hex, :picosat_elixir, "0.1.5", "23673bd3080a4489401e25b4896aff1f1138d47b2f650eab724aad1506188ebb", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b30b3c3abd1f4281902d3b5bc9b67e716509092d6243b010c29d8be4a526e8c8"},
"plug": {:hex, :plug, "1.12.0", "39dc7f1ef8c46bb1bf6dd8f6a49f526c45b4b92ce553687fd885b559a46d0230", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5282c76e89efdf43f2e04bd268ca99d738039f9518137f02ff468cee3ba78096"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.1", "407dcb90755167fd9e3311b60565ff32ed0d234010363406c07cdb4175b95bc5", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "68f4bdb2ac3b594209e54625d3d58c9e2e98b90f2ec8e03235f66e88c9eda5fe"},
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.1", "7cc96ff645158a94cf3ec9744464414f02287f832d6847079adfe0b58761cbd0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "107d0a5865fa92bcb48e631cc0729ae9ccfa0a9f9a1bd8f01acb513abf1c2d64"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"postgrex": {:hex, :postgrex, "0.15.9", "46f8fe6f25711aeb861c4d0ae09780facfdf3adbd2fb5594ead61504dd489bda", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "610719103e4cb2223d4ab78f9f0f3e720320eeca6011415ab4137ddef730adee"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"surface": {:hex, :surface, "0.4.1", "baecbf1f0e008ad19ef5a971e9db4ca1d69e8d72bb6cdae7a09f2bdc3bb4975f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "aa60074ccc3ad05db2b866275da159bf13e3c7f7810ebc0f8a098b44e10e58ad"},
"surface_formatter": {:hex, :surface_formatter, "0.4.1", "1f98b0751e010e94fca722fe6a8fecd03315e9e59d4f7ec3e5609e05d2051e9f", [:mix], [{:surface, "~> 0.4.0", [hex: :surface, repo: "hexpm", optional: false]}], "hexpm", "3dc3c4cabb06315a8f502ee4f240a33a2770830efef1f4b082d29e1e2a307e20"},
"surface": {:hex, :surface, "0.5.1", "2aa593d8ba5dde584e288f697ec8318352f2ff2037ad6d195788c702f104bff2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "a853bfe3479e1f711c84d69d988f85612bd2847dc18ada80be951af40dbe136c"},
"surface_formatter": {:hex, :surface_formatter, "0.5.4", "ce3332e2516615795d10bcf8fb10c765128ff9ccb0fa0e21aa4f384a58498d52", [:mix], [{:surface, "~> 0.5.0", [hex: :surface, repo: "hexpm", optional: false]}], "hexpm", "ea1a5666e4abf1a6c61048fb9a64040ce59865cf33002ca08d2011b6d700feb4"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"timex": {:hex, :timex, "3.7.5", "3eca56e23bfa4e0848f0b0a29a92fa20af251a975116c6d504966e8a90516dfd", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "a15608dca680f2ef663d71c95842c67f0af08a0f3b1d00e17bbd22872e2874e4"},
"timex": {:hex, :timex, "3.7.6", "502d2347ec550e77fdf419bc12d15bdccd31266bb7d925b30bf478268098282f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "a296327f79cb1ec795b896698c56e662ed7210cc9eb31f0ab365eb3a62e2c589"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},

File diff suppressed because one or more lines are too long