chore(portal): DRY live table error handling and handle errors via page reloads (#4205)

I also added whole bunch of tests for the live tables.

Closes #4189
This commit is contained in:
Andrew Dryga
2024-03-19 12:26:41 -06:00
committed by GitHub
parent 370a45571c
commit a339828570
19 changed files with 919 additions and 276 deletions

View File

@@ -26,20 +26,14 @@ defmodule Web.Actors.Index do
def handle_actors_update!(socket, list_opts) do
list_opts = Keyword.put(list_opts, :preload, [:last_seen_at, identities: :provider])
with {:ok, actors, metadata} <- Actors.list_actors(socket.assigns.subject, list_opts) do
{:ok, actor_groups} = Actors.peek_actor_groups(actors, 3, socket.assigns.subject)
assign(socket,
actors: actors,
actors_metadata: metadata,
actor_groups: actor_groups
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
with {:ok, actors, metadata} <- Actors.list_actors(socket.assigns.subject, list_opts),
{:ok, actor_groups} <- Actors.peek_actor_groups(actors, 3, socket.assigns.subject) do
{:ok,
assign(socket,
actors: actors,
actors_metadata: metadata,
actor_groups: actor_groups
)}
end
end

View File

@@ -59,16 +59,11 @@ defmodule Web.Actors.Show do
with {:ok, identities, metadata} <-
Auth.list_identities_for(socket.assigns.actor, socket.assigns.subject, list_opts) do
assign(socket,
identities: identities,
identities_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
identities: identities,
identities_metadata: metadata
)}
end
end
@@ -82,16 +77,11 @@ defmodule Web.Actors.Show do
with {:ok, tokens, metadata} <-
Tokens.list_tokens_for(socket.assigns.actor, socket.assigns.subject, list_opts) do
assign(socket,
tokens: tokens,
tokens_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
tokens: tokens,
tokens_metadata: metadata
)}
end
end
@@ -100,16 +90,11 @@ defmodule Web.Actors.Show do
with {:ok, clients, metadata} <-
Clients.list_clients_for(socket.assigns.actor, socket.assigns.subject, list_opts) do
assign(socket,
clients: clients,
clients_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
clients: clients,
clients_metadata: metadata
)}
end
end
@@ -123,16 +108,11 @@ defmodule Web.Actors.Show do
with {:ok, flows, metadata} <-
Flows.list_flows_for(socket.assigns.actor, socket.assigns.subject, list_opts) do
assign(socket,
flows: flows,
flows_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
flows: flows,
flows_metadata: metadata
)}
end
end

View File

@@ -30,16 +30,11 @@ defmodule Web.Clients.Index do
list_opts = Keyword.put(list_opts, :preload, [:actor, :online?])
with {:ok, clients, metadata} <- Clients.list_clients(socket.assigns.subject, list_opts) do
assign(socket,
clients: clients,
clients_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
clients: clients,
clients_metadata: metadata
)}
end
end

View File

@@ -47,16 +47,11 @@ defmodule Web.Clients.Show do
with {:ok, flows, metadata} <-
Flows.list_flows_for(socket.assigns.client, socket.assigns.subject, list_opts) do
assign(socket,
flows: flows,
flows_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
flows: flows,
flows_metadata: metadata
)}
end
end

View File

@@ -48,16 +48,11 @@ defmodule Web.Groups.EditActors do
list_opts = Keyword.put(list_opts, :preload, identities: :provider)
with {:ok, actors, metadata} <- Actors.list_actors(socket.assigns.subject, list_opts) do
assign(socket,
actors: actors,
actors_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
actors: actors,
actors_metadata: metadata
)}
end
end

View File

@@ -26,20 +26,14 @@ defmodule Web.Groups.Index do
def handle_groups_update!(socket, list_opts) do
list_opts = Keyword.put(list_opts, :preload, [:provider, created_by_identity: [:actor]])
with {:ok, groups, metadata} <- Actors.list_groups(socket.assigns.subject, list_opts) do
{:ok, group_actors} = Actors.peek_group_actors(groups, 3, socket.assigns.subject)
assign(socket,
groups: groups,
groups_metadata: metadata,
group_actors: group_actors
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
with {:ok, groups, metadata} <- Actors.list_groups(socket.assigns.subject, list_opts),
{:ok, group_actors} <- Actors.peek_group_actors(groups, 3, socket.assigns.subject) do
{:ok,
assign(socket,
groups: groups,
groups_metadata: metadata,
group_actors: group_actors
)}
end
end

View File

@@ -50,16 +50,11 @@ defmodule Web.Groups.Show do
list_opts = Keyword.put(list_opts, :preload, [:last_seen_at, identities: :provider])
with {:ok, actors, metadata} <- Actors.list_actors(socket.assigns.subject, list_opts) do
assign(socket,
actors: actors,
actors_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
actors: actors,
actors_metadata: metadata
)}
end
end
@@ -67,16 +62,11 @@ defmodule Web.Groups.Show do
list_opts = Keyword.put(list_opts, :preload, :resource)
with {:ok, policies, metadata} <- Policies.list_policies(socket.assigns.subject, list_opts) do
assign(socket,
policies: policies,
policies_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
policies: policies,
policies_metadata: metadata
)}
end
end

View File

@@ -29,16 +29,11 @@ defmodule Web.Policies.Index do
list_opts = Keyword.put(list_opts, :preload, actor_group: [:provider], resource: [])
with {:ok, policies, metadata} <- Policies.list_policies(socket.assigns.subject, list_opts) do
assign(socket,
policies: policies,
policies_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
policies: policies,
policies_metadata: metadata
)}
end
end

View File

@@ -45,16 +45,11 @@ defmodule Web.Policies.Show do
with {:ok, flows, metadata} <-
Flows.list_flows_for(socket.assigns.policy, socket.assigns.subject, list_opts) do
assign(socket,
flows: flows,
flows_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
flows: flows,
flows_metadata: metadata
)}
end
end

View File

@@ -33,16 +33,11 @@ defmodule Web.RelayGroups.Index do
list_opts = Keyword.put(list_opts, :preload, relays: [:online?])
with {:ok, groups, metadata} <- Relays.list_groups(socket.assigns.subject, list_opts) do
assign(socket,
groups: groups,
groups_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
groups: groups,
groups_metadata: metadata
)}
end
end

View File

@@ -49,16 +49,11 @@ defmodule Web.RelayGroups.Show do
list_opts = Keyword.put(list_opts, :preload, [:online?])
with {:ok, relays, metadata} <- Relays.list_relays(socket.assigns.subject, list_opts) do
assign(socket,
relays: relays,
relays_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
relays: relays,
relays_metadata: metadata
)}
end
end

View File

@@ -31,21 +31,15 @@ defmodule Web.Resources.Index do
list_opts = Keyword.put(list_opts, :preload, [:gateway_groups])
with {:ok, resources, metadata} <-
Resources.list_resources(socket.assigns.subject, list_opts) do
{:ok, resource_actor_groups_peek} =
Resources.peek_resource_actor_groups(resources, 3, socket.assigns.subject)
assign(socket,
resources: resources,
resource_actor_groups_peek: resource_actor_groups_peek,
resources_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
Resources.list_resources(socket.assigns.subject, list_opts),
{:ok, resource_actor_groups_peek} <-
Resources.peek_resource_actor_groups(resources, 3, socket.assigns.subject) do
{:ok,
assign(socket,
resources: resources,
resource_actor_groups_peek: resource_actor_groups_peek,
resources_metadata: metadata
)}
end
end

View File

@@ -55,16 +55,11 @@ defmodule Web.Resources.Show do
list_opts = Keyword.put(list_opts, :preload, actor_group: [:provider], resource: [])
with {:ok, policies, metadata} <- Policies.list_policies(socket.assigns.subject, list_opts) do
assign(socket,
policies: policies,
policies_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
policies: policies,
policies_metadata: metadata
)}
end
end
@@ -78,16 +73,11 @@ defmodule Web.Resources.Show do
with {:ok, flows, metadata} <-
Flows.list_flows_for(socket.assigns.resource, socket.assigns.subject, list_opts) do
assign(socket,
flows: flows,
flows_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
flows: flows,
flows_metadata: metadata
)}
end
end

View File

@@ -36,16 +36,11 @@ defmodule Web.Settings.IdentityProviders.Index do
def handle_providers_update!(socket, list_opts) do
with {:ok, providers, metadata} <- Auth.list_providers(socket.assigns.subject, list_opts) do
assign(socket,
providers: providers,
providers_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
providers: providers,
providers_metadata: metadata
)}
end
end

View File

@@ -41,16 +41,11 @@ defmodule Web.Sites.Gateways.Index do
list_opts = Keyword.put(list_opts, :preload, [:online?])
with {:ok, gateways, metadata} <- Gateways.list_gateways(socket.assigns.subject, list_opts) do
assign(socket,
gateways: gateways,
gateways_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
gateways: gateways,
gateways_metadata: metadata
)}
end
end

View File

@@ -31,16 +31,11 @@ defmodule Web.Sites.Index do
with {:ok, groups, metadata} <-
Gateways.list_groups(socket.assigns.subject, list_opts) do
assign(socket,
groups: groups,
groups_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
groups: groups,
groups_metadata: metadata
)}
end
end

View File

@@ -63,36 +63,25 @@ defmodule Web.Sites.Show do
end)
with {:ok, gateways, metadata} <- Gateways.list_gateways(socket.assigns.subject, list_opts) do
assign(socket,
gateways: gateways,
gateways_metadata: metadata
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
{:ok,
assign(socket,
gateways: gateways,
gateways_metadata: metadata
)}
end
end
def handle_resources_update!(socket, list_opts) do
with {:ok, resources, metadata} <-
Resources.list_resources(socket.assigns.subject, list_opts) do
{:ok, resource_actor_groups_peek} =
Resources.peek_resource_actor_groups(resources, 3, socket.assigns.subject)
assign(socket,
resources: resources,
resources_metadata: metadata,
resource_actor_groups_peek: resource_actor_groups_peek
)
else
{:error, :invalid_cursor} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} -> raise Web.LiveErrors.InvalidRequestError
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
Resources.list_resources(socket.assigns.subject, list_opts),
{:ok, resource_actor_groups_peek} <-
Resources.peek_resource_actor_groups(resources, 3, socket.assigns.subject) do
{:ok,
assign(socket,
resources: resources,
resources_metadata: metadata,
resource_actor_groups_peek: resource_actor_groups_peek
)}
end
end

View File

@@ -82,7 +82,13 @@ defmodule Web.LiveTable do
defp resource_filter(assigns) do
~H"""
<.form id={"#{@live_table_id}-filters"} for={@form} phx-change="filter" phx-debounce="100">
<.form
:if={@filters != []}
id={"#{@live_table_id}-filters"}
for={@form}
phx-change="filter"
phx-debounce="100"
>
<.input type="hidden" name="table_id" value={@live_table_id} />
<div
@@ -423,7 +429,11 @@ defmodule Web.LiveTable do
def reload_live_table!(socket, id) do
callback = Map.fetch!(socket.assigns.callback_by_table_id, id)
list_opts = Map.get(socket.assigns[:list_opts_by_table_id] || %{}, id, [])
callback.(socket, list_opts)
case callback.(socket, list_opts) do
{:error, _reason} -> push_navigate(socket, to: socket.assigns.uri)
{:ok, socket} -> socket
end
end
@doc """
@@ -457,31 +467,48 @@ defmodule Web.LiveTable do
order_by: List.wrap(order_by)
]
socket
|> maybe_apply_callback(id, list_opts)
|> assign(
filter_form_by_table_id:
put_table_state(
socket,
id,
:filter_form_by_table_id,
filter_to_form(filter, id)
),
order_by_table_id:
put_table_state(
socket,
id,
:order_by_table_id,
order_by
),
list_opts_by_table_id:
put_table_state(
socket,
id,
:list_opts_by_table_id,
list_opts
case maybe_apply_callback(socket, id, list_opts) do
{:ok, socket} ->
socket
|> assign(
filter_form_by_table_id:
put_table_state(
socket,
id,
:filter_form_by_table_id,
filter_to_form(filter, id)
),
order_by_table_id:
put_table_state(
socket,
id,
:order_by_table_id,
order_by
),
list_opts_by_table_id:
put_table_state(
socket,
id,
:list_opts_by_table_id,
list_opts
)
)
)
{:error, :invalid_cursor} ->
raise Web.LiveErrors.InvalidRequestError
{:error, {:unknown_filter, _metadata}} ->
raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_type, _metadata}} ->
raise Web.LiveErrors.InvalidRequestError
{:error, {:invalid_value, _metadata}} ->
raise Web.LiveErrors.InvalidRequestError
{:error, _reason} ->
raise Web.LiveErrors.NotFoundError
end
end
defp maybe_apply_callback(socket, id, list_opts) do
@@ -491,7 +518,7 @@ defmodule Web.LiveTable do
callback = Map.fetch!(socket.assigns.callback_by_table_id, id)
callback.(socket, list_opts)
else
socket
{:ok, socket}
end
end
@@ -529,7 +556,8 @@ defmodule Web.LiveTable do
ArgumentError -> []
end
defp filter_to_form(filter, as) do
@doc false
def filter_to_form(filter, as) do
# Note: we don't support nesting, :and or :where on the UI yet
for {key, value} <- filter, into: %{} do
{Atom.to_string(key), value}

View File

@@ -0,0 +1,734 @@
defmodule Web.LiveTableTest do
use Web.ConnCase, async: true
import Web.LiveTable
describe "<.live_table /> component" do
setup do
assigns = %{
id: "table-id",
filters: [],
filter: filter_to_form(%{}, "table-id"),
ordered_by: {:assoc, :name},
metadata: %{
previous_page_cursor: nil,
next_page_cursor: nil,
limit: 10,
count: 1
},
col: [
%{
label: "name",
field: {:assoc, :name},
inner_block: fn _col, row ->
row
end
}
],
rows: ["foo"]
}
%{assigns: assigns}
end
test "renders a data table", %{assigns: assigns} do
html = render_component(&live_table/1, assigns)
assert html
|> Floki.find("table")
|> Floki.attribute("id") == ["table-id"]
assert html
|> Floki.find("table thead")
|> Floki.attribute("id") == ["table-id-header"]
assert html
|> Floki.find("th")
|> Floki.text() =~ "name"
assert html
|> Floki.find("table tbody")
|> Floki.attribute("id") == ["table-id-rows"]
assert html
|> Floki.find("td")
|> Floki.text() =~ "foo"
end
test "renders fulltext search filter", %{assigns: assigns} do
assigns = %{
assigns
| filters: [
%Domain.Repo.Filter{
name: :search,
title: "Query",
type: {:string, :websearch}
}
],
filter: filter_to_form(%{search: "foo"}, "table-id")
}
form =
render_component(&live_table/1, assigns)
|> Floki.find("form")
assert Floki.attribute(form, "id") == ["table-id-filters"]
assert Floki.attribute(form, "phx-change") == ["filter"]
input = Floki.find(form, "input[type=hidden]")
assert Floki.attribute(input, "name") == ["table_id"]
assert Floki.attribute(input, "value") == ["table-id"]
input = Floki.find(form, "input[type=text]")
assert Floki.attribute(input, "id") == ["table-id_search"]
assert Floki.attribute(input, "name") == ["table-id[search]"]
assert Floki.attribute(input, "placeholder") == ["Search by Query"]
assert Floki.attribute(input, "value") == ["foo"]
end
test "renders email filter", %{assigns: assigns} do
assigns = %{
assigns
| filters: [
%Domain.Repo.Filter{
name: :email,
title: "Email",
type: {:string, :email}
}
],
filter: filter_to_form(%{email: "foo@bar.com"}, "table-id")
}
form =
render_component(&live_table/1, assigns)
|> Floki.find("form")
assert Floki.attribute(form, "id") == ["table-id-filters"]
assert Floki.attribute(form, "phx-change") == ["filter"]
input = Floki.find(form, "input[type=hidden]")
assert Floki.attribute(input, "name") == ["table_id"]
assert Floki.attribute(input, "value") == ["table-id"]
input = Floki.find(form, "input[type=text]")
assert Floki.attribute(input, "id") == ["table-id_email"]
assert Floki.attribute(input, "name") == ["table-id[email]"]
assert Floki.attribute(input, "placeholder") == ["Search by Email"]
assert Floki.attribute(input, "value") == ["foo@bar.com"]
end
test "renders UUID dropdown filter", %{assigns: assigns} do
assigns = %{
assigns
| filters: [
%Domain.Repo.Filter{
name: :id,
title: "ID",
type: {:string, :uuid},
values: [
{"group1", [{"One", "1"}]},
{"group1", [{"Two", "2"}]},
{nil, [{"Three", "3"}]}
]
}
],
filter: filter_to_form(%{id: "1"}, "table-id")
}
form =
render_component(&live_table/1, assigns)
|> Floki.find("form")
assert Floki.attribute(form, "id") == ["table-id-filters"]
assert Floki.attribute(form, "phx-change") == ["filter"]
input = Floki.find(form, "input[type=hidden]")
assert Floki.attribute(input, "name") == ["table_id"]
assert Floki.attribute(input, "value") == ["table-id"]
select = Floki.find(form, "select")
assert Floki.attribute(select, "id") == ["table-id_id"]
assert Floki.attribute(select, "name") == ["table-id[id]"]
assert select |> List.first() |> elem(2) == [
{"option", [{"value", ""}], ["For any ID"]},
{"optgroup", [{"label", "group1"}],
[{"option", [{"selected", "selected"}, {"value", "1"}], ["One"]}]},
{"optgroup", [{"label", "group1"}], [{"option", [{"value", "2"}], ["Two"]}]},
{"option", [{"value", "3"}], ["Three"]}
]
end
test "renders radio buttons for select from up to 5 values", %{assigns: assigns} do
assigns = %{
assigns
| filters: [
%Domain.Repo.Filter{
name: :btn,
title: "Button",
type: :string,
values: [
{"One", "1"},
{"Two", "2"}
]
}
],
filter: filter_to_form(%{id: "1"}, "table-id")
}
form =
render_component(&live_table/1, assigns)
|> Floki.find("form")
assert Floki.attribute(form, "id") == ["table-id-filters"]
assert Floki.attribute(form, "phx-change") == ["filter"]
input = Floki.find(form, "input[type=hidden]")
assert Floki.attribute(input, "name") == ["table_id"]
assert Floki.attribute(input, "value") == ["table-id"]
radio = Floki.find(form, "input[type=radio]")
assert Floki.attribute(radio, "id") == [
"table-id-btn-__all__",
"table-id-btn-1",
"table-id-btn-2"
]
assert Floki.attribute(radio, "name") == [
"_reset:table-id[btn]",
"table-id[btn]",
"table-id[btn]"
]
assert Floki.attribute(radio, "value") == [
"true",
"1",
"2"
]
end
test "renders checkbox-like buttons for multi-select from up to 5 values", %{assigns: assigns} do
assigns = %{
assigns
| filters: [
%Domain.Repo.Filter{
name: :btn,
title: "Button",
type: {:list, :string},
values: [
{"One", "1"},
{"Two", "2"}
]
}
],
filter: filter_to_form(%{id: "1"}, "table-id")
}
form =
render_component(&live_table/1, assigns)
|> Floki.find("form")
assert Floki.attribute(form, "id") == ["table-id-filters"]
assert Floki.attribute(form, "phx-change") == ["filter"]
input = Floki.find(form, "input[type=hidden]")
assert Floki.attribute(input, "name") == ["table_id"]
assert Floki.attribute(input, "value") == ["table-id"]
radio = Floki.find(form, "input[type=checkbox]")
assert Floki.attribute(radio, "id") == [
"table-id-btn-__all__",
"table-id-btn-1",
"table-id-btn-2"
]
assert Floki.attribute(radio, "name") == [
"_reset:table-id[btn]",
"table-id[btn][]",
"table-id[btn][]"
]
assert Floki.attribute(radio, "value") == [
"value",
"1",
"2"
]
end
test "renders value dropdown when there are more than 5 values", %{assigns: assigns} do
assigns = %{
assigns
| filters: [
%Domain.Repo.Filter{
name: :select,
title: "Select",
type: :string,
values: [
{"One", "1"},
{"Two", "2"},
{"Three", "3"},
{"Four", "4"},
{"Five", "5"},
{"Six", "6"}
]
}
],
filter: filter_to_form(%{id: "1"}, "table-id")
}
form =
render_component(&live_table/1, assigns)
|> Floki.find("form")
assert Floki.attribute(form, "id") == ["table-id-filters"]
assert Floki.attribute(form, "phx-change") == ["filter"]
input = Floki.find(form, "input[type=hidden]")
assert Floki.attribute(input, "name") == ["table_id"]
assert Floki.attribute(input, "value") == ["table-id"]
select = Floki.find(form, "select")
assert Floki.attribute(select, "id") == ["table-id_select"]
assert Floki.attribute(select, "name") == ["table-id[select]"]
assert select |> List.first() |> elem(2) == [
{"option", [{"value", ""}], ["For any Select"]},
{"optgroup", [{"label", "Select"}],
[
{"option", [{"value", "1"}], ["One"]},
{"option", [{"value", "2"}], ["Two"]},
{"option", [{"value", "3"}], ["Three"]},
{"option", [{"value", "4"}], ["Four"]},
{"option", [{"value", "5"}], ["Five"]},
{"option", [{"value", "6"}], ["Six"]}
]}
]
end
test "renders ordering buttons", %{assigns: assigns} do
# default order when it's unset
html = render_component(&live_table/1, assigns)
order_button = Floki.find(html, "th button")
assert Floki.attribute(order_button, "phx-click") == ["order_by"]
assert Floki.attribute(order_button, "phx-value-table_id") == ["table-id"]
assert Floki.attribute(order_button, "phx-value-order_by") == ["assoc:asc:name"]
# current order if it's set
assigns = %{assigns | ordered_by: {:assoc, :desc, :name}}
html = render_component(&live_table/1, assigns)
order_button = Floki.find(html, "th button")
assert Floki.attribute(order_button, "phx-value-order_by") == ["assoc:desc:name"]
end
test "renders page size and total count", %{assigns: assigns} do
assert render_component(&live_table/1, assigns)
|> Floki.find("nav > span")
|> Floki.text()
|> String.replace(~r/[\s]+/, " ") =~ "Showing 1 of 1"
assert render_component(&live_table/1, %{
assigns
| metadata: %{assigns.metadata | count: 10, limit: 100}
})
|> Floki.find("nav > span")
|> Floki.text()
|> String.replace(~r/[\s]+/, " ") =~ "Showing 10 of 10"
assert render_component(&live_table/1, %{
assigns
| metadata: %{assigns.metadata | count: 100, limit: 10}
})
|> Floki.find("nav > span")
|> Floki.text()
|> String.replace(~r/[\s]+/, " ") =~ "Showing 10 of 100"
end
test "renders pagination buttons", %{assigns: assigns} do
html = render_component(&live_table/1, assigns)
assert html
|> Floki.find("nav button")
|> Floki.attribute("disabled") == ["disabled", "disabled"]
assigns = %{assigns | metadata: %{assigns.metadata | next_page_cursor: "next_cursor"}}
html = render_component(&live_table/1, assigns)
assert html
|> Floki.find("nav button")
|> Floki.attribute("disabled") == ["disabled"]
enabled_button = Floki.find(html, "nav button:not([disabled])")
assert Floki.attribute(enabled_button, "phx-click") == ["paginate"]
assert Floki.attribute(enabled_button, "phx-value-cursor") == ["next_cursor"]
assert Floki.attribute(enabled_button, "phx-value-table_id") == ["table-id"]
assigns = %{assigns | metadata: %{assigns.metadata | previous_page_cursor: "prev_cursor"}}
html = render_component(&live_table/1, assigns)
assert html
|> Floki.find("nav button")
|> Floki.attribute("disabled") == []
enabled_button = Floki.find(html, "nav button:not([disabled])")
assert "prev_cursor" in Floki.attribute(enabled_button, "phx-value-cursor")
end
end
describe "assign_live_table/3" do
setup do
subject = Fixtures.Auth.create_subject()
socket = %Phoenix.LiveView.Socket{assigns: %{subject: subject, __changed__: %{}}}
%{socket: socket}
end
test "persists live table state in the socket", %{socket: socket} do
assert %{
assigns: %{
__changed__: %{
live_table_ids: true,
callback_by_table_id: true,
sortable_fields_by_table_id: true,
filters_by_table_id: true,
enforced_filters_by_table_id: true,
limit_by_table_id: true
},
live_table_ids: ["table-id"],
callback_by_table_id: %{"table-id" => _fun},
sortable_fields_by_table_id: %{"table-id" => [actors: :name]},
filters_by_table_id: %{"table-id" => []},
enforced_filters_by_table_id: %{},
limit_by_table_id: %{}
}
} =
assign_live_table(socket, "table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
callback: fn socket, list_opts ->
{:ok, %{socket | private: %{list_opts: list_opts}}}
end
)
assert %{
assigns: %{
__changed__: %{
live_table_ids: true,
callback_by_table_id: true,
sortable_fields_by_table_id: true,
filters_by_table_id: true,
enforced_filters_by_table_id: true,
limit_by_table_id: true
},
live_table_ids: ["table-id"],
callback_by_table_id: %{"table-id" => _fun},
sortable_fields_by_table_id: %{"table-id" => [actors: :name]},
filters_by_table_id: %{"table-id" => []},
enforced_filters_by_table_id: %{"table-id" => [name: "foo"]},
limit_by_table_id: %{"table-id" => 11}
}
} =
assign_live_table(socket, "table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
enforce_filters: [
{:name, "foo"}
],
hide_filters: [:email],
limit: 11,
callback: fn socket, list_opts ->
{:ok, %{socket | private: %{list_opts: list_opts}}}
end
)
end
end
describe "reload_live_table!/2" do
test "reloads the live table" do
subject = Fixtures.Auth.create_subject()
socket =
%Phoenix.LiveView.Socket{
assigns: %{subject: subject, __changed__: %{}}
}
|> assign_live_table("table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
callback: fn socket, list_opts ->
{:ok, %{socket | private: %{list_opts: list_opts}}}
end
)
assert %{
private: %{list_opts: []}
} = reload_live_table!(socket, "table-id")
end
test "reloads whole page on errors" do
subject = Fixtures.Auth.create_subject()
socket =
%Phoenix.LiveView.Socket{
assigns: %{subject: subject, uri: "/current_uri", __changed__: %{}}
}
|> assign_live_table("table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
callback: fn _socket, _list_opts -> {:error, :not_found} end
)
assert %{
redirected: {:live, :redirect, %{kind: :push, to: "/current_uri"}}
} = reload_live_table!(socket, "table-id")
end
end
describe "handle_live_tables_params/3" do
setup do
subject = Fixtures.Auth.create_subject()
test_pid = self()
socket =
%Phoenix.LiveView.Socket{
assigns: %{subject: subject, __changed__: %{}}
}
|> assign_live_table("table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
callback: fn socket, list_opts ->
send(test_pid, {:callback, socket, list_opts})
{:ok, %{socket | private: %{list_opts: list_opts}}}
end
)
%{socket: socket}
end
test "assigns the live table data from callbacks", %{socket: socket} do
assert %{
assigns: %{
uri: "/actors"
},
private: %{
list_opts: [
page: [limit: 25],
filter: [],
order_by: []
]
}
} = handle_live_tables_params(socket, %{}, "/actors")
assert %{
assigns: %{
uri: "/actors"
},
private: %{
list_opts: [
page: [cursor: "next_page", limit: 25],
filter: [{:name, "foo"}],
order_by: [{:actors, :asc, :name}]
]
}
} =
handle_live_tables_params(
socket,
%{
"table-id_cursor" => "next_page",
"table-id_filter" => %{"name" => "foo"},
"table-id_order_by" => "actors:asc:name"
},
"/actors"
)
end
test "does nothing when list opts are not changed", %{socket: socket} do
socket = handle_live_tables_params(socket, %{}, "/actors")
assert_receive {:callback, _socket, [page: [limit: 25], filter: [], order_by: []]}
handle_live_tables_params(socket, %{}, "/actors")
refute_receive {:callback, _socket, _list_opts}
end
test "raises if the callback returns an error" do
subject = Fixtures.Auth.create_subject()
socket =
%Phoenix.LiveView.Socket{
assigns: %{subject: subject, uri: "/current_uri", __changed__: %{}}
}
for {reason, exception} <- [
{:not_found, Web.LiveErrors.NotFoundError},
{:unauthorized, Web.LiveErrors.NotFoundError},
{:invalid_cursor, Web.LiveErrors.InvalidRequestError},
{{:unknown_filter, []}, Web.LiveErrors.InvalidRequestError},
{{:invalid_type, []}, Web.LiveErrors.InvalidRequestError},
{{:invalid_value, []}, Web.LiveErrors.InvalidRequestError}
] do
socket =
assign_live_table(socket, "table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
callback: fn _socket, _list_opts -> {:error, reason} end
)
assert_raise exception, fn ->
handle_live_tables_params(socket, %{}, "/foo")
end
end
end
end
describe "handle_live_table_event/3 for pagination" do
setup do
subject = Fixtures.Auth.create_subject()
socket =
%Phoenix.LiveView.Socket{
assigns: %{
subject: subject,
uri:
"/actors?table-id_cursor=prev_page" <>
"&table-id_filter%5Bname%5D=buz" <>
"&table-id_order_by=actors%3Aasc%3Aname",
__changed__: %{}
}
}
|> assign_live_table("table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
callback: fn socket, list_opts ->
{:ok, %{socket | private: %{list_opts: list_opts}}}
end
)
%{socket: socket}
end
test "updates query parameters with new cursor", %{socket: socket} do
assert handle_live_table_event(
"paginate",
%{"table_id" => "table-id", "cursor" => "very_next_page"},
socket
)
|> fetch_patched_query_params!() == %{
"table-id_order_by" => "actors:asc:name",
"table-id_cursor" => "very_next_page",
"table-id_filter[name]" => "buz"
}
end
end
describe "handle_live_table_event/3 for filtering" do
setup do
subject = Fixtures.Auth.create_subject()
socket =
%Phoenix.LiveView.Socket{
assigns: %{
subject: subject,
uri:
"/actors?table-id_cursor=next_page" <>
"&table-id_filter%5Bemail%5D=bar" <>
"&table-id_filter%5Bname%5D=buz" <>
"&table-id_order_by=actors%3Aasc%3Aname",
__changed__: %{}
}
}
|> assign_live_table("table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
callback: fn socket, list_opts ->
{:ok, %{socket | private: %{list_opts: list_opts}}}
end
)
%{socket: socket}
end
test "resets the query parameters with filter value is set to all", %{socket: socket} do
assert handle_live_table_event(
"filter",
%{"table_id" => "table-id", "_target" => ["_reset:table-id", "name"]},
socket
)
|> fetch_patched_query_params!() == %{
"table-id_filter[email]" => "bar",
"table-id_order_by" => "actors:asc:name"
}
end
test "updates query parameters with new filter and resets the cursor", %{socket: socket} do
assert handle_live_table_event(
"filter",
%{"table_id" => "table-id", "table-id" => %{"name" => "foo"}},
socket
)
|> fetch_patched_query_params!() == %{
"table-id_filter[name]" => "foo",
"table-id_order_by" => "actors:asc:name"
}
end
end
describe "handle_live_table_event/3 for ordering" do
setup do
subject = Fixtures.Auth.create_subject()
socket =
%Phoenix.LiveView.Socket{
assigns: %{
subject: subject,
uri:
"/actors?table-id_cursor=next_page&table-id_filter%5Bname%5D=bar&table-id_order_by=actors%3Aasc%3Aname",
__changed__: %{}
}
}
|> assign_live_table("table-id",
query_module: Actors.Actor.Query,
sortable_fields: [
{:actors, :name}
],
callback: fn socket, list_opts ->
{:ok, %{socket | private: %{list_opts: list_opts}}}
end
)
%{socket: socket}
end
test "updates query parameters with reverse order and resets the cursor", %{socket: socket} do
assert handle_live_table_event(
"order_by",
%{"table_id" => "table-id", "order_by" => "actors:desc:name"},
socket
)
|> fetch_patched_query_params!() == %{
"table-id_filter[name]" => "bar",
"table-id_order_by" => "actors:asc:name"
}
end
end
defp fetch_patched_query_params!(socket) do
assert {:noreply, %{redirected: {:live, :patch, %{kind: :push, to: to}}}} = socket
uri = URI.parse(to)
URI.decode_query(uri.query)
end
end