Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error Pages #331

Merged
merged 53 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9bbff74
Beacon install integration test
APB9785 Jul 26, 2023
bdf469e
Test data_source, mixfile, seeds
APB9785 Jul 27, 2023
9a57b47
Credo
APB9785 Jul 27, 2023
836c666
Fixes
APB9785 Jul 28, 2023
be197d9
Bump tailwind version
APB9785 Jul 28, 2023
1a398c5
Idempotence tests
APB9785 Aug 16, 2023
0ef6e1a
Merge branch 'main' into apb/test-beacon-install
APB9785 Aug 16, 2023
6b93d12
Update template
APB9785 Aug 16, 2023
cd6f223
Resolve compile warnings
APB9785 Aug 16, 2023
74f7397
Schema
APB9785 Aug 16, 2023
00d258c
Context functions
APB9785 Aug 16, 2023
6aa9c5a
Migration
APB9785 Aug 16, 2023
7df36dc
Update docs
APB9785 Aug 16, 2023
08b807a
Inject in mix task
APB9785 Aug 17, 2023
293c826
Merge branch 'apb/test-beacon-install' into apb/error-pages
APB9785 Aug 17, 2023
9fd3aaa
Test
APB9785 Aug 17, 2023
917e2db
Error pages backend
APB9785 Aug 23, 2023
f8ba2e6
Minor fixes
APB9785 Aug 23, 2023
af3fb58
Merge branch 'main' into apb/error-pages
APB9785 Aug 23, 2023
1edaa3a
Fix typespec
APB9785 Aug 23, 2023
c30a6f9
Unique index
APB9785 Aug 24, 2023
fbe93e8
Add test for create error
APB9785 Aug 24, 2023
4026c75
Use error layout
APB9785 Aug 24, 2023
cefe1c7
Small doc fixes
APB9785 Aug 24, 2023
7d9debc
Validate error status codes
APB9785 Aug 28, 2023
932b11f
Switch error pages to module recompilation
APB9785 Aug 28, 2023
0062c21
CI fixes
APB9785 Aug 28, 2023
3c4b53f
Fix test fixtures
APB9785 Aug 28, 2023
7730474
render
APB9785 Aug 28, 2023
89bd7c5
test render/1
leandrocp Aug 28, 2023
8bd071a
publish default layouts
leandrocp Aug 29, 2023
aa157fe
Improve test fixture
APB9785 Aug 29, 2023
127fb48
Test custom error page
APB9785 Aug 29, 2023
fc63031
PubSub
APB9785 Aug 29, 2023
7a6a1dd
Merge branch 'main' into apb/error-pages
APB9785 Aug 30, 2023
4aa9fd4
Still not working
APB9785 Aug 30, 2023
e6928c1
render error page with root layout
leandrocp Aug 31, 2023
6d14037
fix layout path
leandrocp Aug 31, 2023
1e3f8b4
pass @conn to root_layout
leandrocp Aug 31, 2023
7d7c7af
test ErrorHTML
leandrocp Aug 31, 2023
ea6f567
Check render and layout for my_component
APB9785 Aug 31, 2023
c796809
fix tests and cleanup
leandrocp Aug 31, 2023
4bc389d
add logger when error page render fails
leandrocp Aug 31, 2023
fff043f
use Phoenix.HTML.Engine
leandrocp Aug 31, 2023
b38f90d
add tests rendering my_component
leandrocp Aug 31, 2023
a08de10
Merge branch 'main' into apb/error-pages
APB9785 Sep 22, 2023
8324828
Merge branch 'apb/error-pages' of https://github.com/BeaconCMS/beacon…
APB9785 Sep 22, 2023
f68c6cb
remove support for components in error pages
leandrocp Oct 12, 2023
b2d00b3
Merge branch 'main' into apb/error-pages
leandrocp Oct 12, 2023
42fbe73
broadcast error_page_updated event only on update_error_page/2
leandrocp Oct 12, 2023
9eeaddb
fix dialyzer
leandrocp Oct 12, 2023
300dbb5
fix dialyzer 2
leandrocp Oct 12, 2023
fa887f0
compile error page content and reload css
leandrocp Oct 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions lib/beacon/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule Beacon.Content do
import Ecto.Query

alias Beacon.Content.Component
alias Beacon.Content.ErrorPage
alias Beacon.Content.Layout
alias Beacon.Content.LayoutEvent
alias Beacon.Content.LayoutSnapshot
Expand Down Expand Up @@ -73,6 +74,18 @@ defmodule Beacon.Content do
Layout.changeset(layout, attrs)
end

@doc """
Returns a map of attrs to load the default layout into new sites.
"""
@spec default_layout() :: map()
@doc type: :layouts
def default_layout do
%{
title: "Default",
template: "<%= @inner_content %>"
}
end

@doc """
Creates a layout.

Expand Down Expand Up @@ -1667,6 +1680,108 @@ defmodule Beacon.Content do
end
end

# ERROR PAGES

@doc """
Returns an `%Ecto.Changeset{}` for tracking error page changes.

## Example

iex> change_error_page(error_page, %{status: 404})
%Ecto.Changeset{data: %ErrorPage{}}

"""
@doc type: :error_pages
@spec change_error_page(ErrorPage.t(), map()) :: Changeset.t()
def change_error_page(%ErrorPage{} = error_page, attrs \\ %{}) do
ErrorPage.changeset(error_page, attrs)
end

@doc """
Returns the error page for a given site and status code, or `nil` if no matching error page exists.
"""
@doc type: :error_pages
@spec get_error_page(Site.t(), integer()) :: ErrorPage.t() | nil
def get_error_page(site, status) do
APB9785 marked this conversation as resolved.
Show resolved Hide resolved
Repo.one(
from e in ErrorPage,
where: e.site == ^site,
where: e.status == ^status
)
end

@doc """
Lists all error pages for a given site.
"""
@doc type: :error_pages
@spec list_error_pages(Site.t()) :: [ErrorPage.t()]
def list_error_pages(site) do
Repo.all(
from e in ErrorPage,
where: e.site == ^site,
order_by: e.status
)
end

@doc """
Creates a new error page.
"""
@doc type: :error_pages
@spec create_error_page(%{site: Site.t(), status: integer(), template: binary(), layout_id: Ecto.UUID.t()}) ::
APB9785 marked this conversation as resolved.
Show resolved Hide resolved
{:ok, ErrorPage.t()} | {:error, Changeset.t()}
def create_error_page(attrs) do
%ErrorPage{}
|> ErrorPage.changeset(attrs)
|> Repo.insert()
end

@doc """
Creates a new error page, raising if the operation fails.
"""
@doc type: :error_pages
@spec create_error_page!(%{site: Site.t(), status: integer(), template: binary(), layout_id: Ecto.UUID.t()}) ::
APB9785 marked this conversation as resolved.
Show resolved Hide resolved
ErrorPage.t()
def create_error_page!(attrs) do
case create_error_page(attrs) do
{:ok, error_page} -> error_page
{:error, changeset} -> raise "failed to create error page, got: #{inspect(changeset.errors)}"
end
end

@doc """
Returns attr data to load the default error_pages into new sites.
"""
@spec default_error_pages() :: [map()]
@doc type: :error_pages
def default_error_pages do
for status <- [404, 500] do
%{
status: status,
template: Plug.Conn.Status.reason_phrase(status)
leandrocp marked this conversation as resolved.
Show resolved Hide resolved
}
end
end

@doc """
Updates an error page.
"""
@doc type: :error_pages
@spec update_error_page(ErrorPage.t(), map()) :: {:ok, ErrorPage.t()} | {:error, Changeset.t()}
def update_error_page(error_page, attrs) do
error_page
|> ErrorPage.changeset(attrs)
|> Repo.update()
end

@doc """
Deletes an error page.
"""
@doc type: :error_pages
@spec delete_error_page(ErrorPage.t()) :: {:ok, ErrorPage.t()} | {:error, Changeset.t()}
def delete_error_page(error_page) do
Repo.delete(error_page)
end

# PAGE EVENT HANDLERS

@doc """
Expand Down
55 changes: 55 additions & 0 deletions lib/beacon/content/error_page.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defmodule Beacon.Content.ErrorPage do
@moduledoc """
Stores a template which can be rendered for error responses.

An ErrorPage contains four main fields:

* `:status` - the status code for which this ErrorPage is to be used
* `:template` - the template to be rendered
* `:site` - the Beacon site which should use this page
* `:layout_id` - the ID of the Beacon Layout which is used for rendering

> #### Do not create or edit ErrorPages manually {: .warning}
>
> Use the public functions in `Beacon.Content` instead.
> The functions in that module guarantee that all dependencies
> are created correctly and all processes are updated.
> Manipulating data manually will most likely result
> in inconsistent behavior and crashes.

"""
use Beacon.Schema

import Ecto.Changeset

@type t :: %__MODULE__{
id: Ecto.UUID.t(),
site: Beacon.Types.Site.t(),
status: integer,
template: binary(),
layout_id: Ecto.UUID.t(),
layout: Beacon.Content.Layout.t(),
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "beacon_error_pages" do
field :site, Beacon.Types.Site
field :status, :integer
field :template, :string

belongs_to :layout, Beacon.Content.Layout

timestamps()
end

@doc false
def changeset(%__MODULE__{} = error_page, attrs) do
fields = ~w(status template site layout_id)a

error_page
|> cast(attrs, fields)
|> validate_required(fields)
|> unique_constraint([:status, :site])
end
end
47 changes: 42 additions & 5 deletions lib/beacon/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,21 @@ defmodule Beacon.Loader do

@doc false
def init(config) do
%{site: site} = config

if Code.ensure_loaded?(Mix.Project) and Mix.env() == :test do
:skip
else
with :ok <- populate_components(config.site) do
:ok = load_site_from_db(config.site)
with :ok <- populate_components(site),
:ok <- populate_layouts(site),
:ok <- populate_error_pages(site) do
:ok = load_site_from_db(site)
end
end

PubSub.subscribe_to_layouts(config.site)
PubSub.subscribe_to_pages(config.site)
PubSub.subscribe_to_components(config.site)
PubSub.subscribe_to_layouts(site)
PubSub.subscribe_to_pages(site)
PubSub.subscribe_to_components(site)

{:ok, config}
end
Expand All @@ -67,6 +71,39 @@ defmodule Beacon.Loader do
:ok
end

defp populate_layouts(site) do
case Content.get_layout_by(site, title: "Default") do
nil ->
Content.default_layout()
|> Map.put(:site, site)
|> Content.create_layout!()

_ ->
:skip
end

:ok
end

defp populate_error_pages(site) do
default_layout = Content.get_layout_by(site, title: "Default")

for attrs <- Content.default_error_pages() do
case Content.get_error_page(site, attrs.status) do
nil ->
attrs
|> Map.put(:site, site)
|> Map.put(:layout_id, default_layout.id)
|> Content.create_error_page!()

_ ->
:skip
end
end

:ok
end

defp load_site_from_db(site) do
with :ok <- Beacon.RuntimeJS.load(),
:ok <- load_runtime_css(site),
Expand Down
28 changes: 14 additions & 14 deletions lib/beacon_web/controllers/error_html.ex
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
defmodule BeaconWeb.ErrorHTML do
@moduledoc false

use BeaconWeb, :html

# If you want to customize your error pages,
# uncomment the embed_templates/1 call below
# and add pages to the error directory:
#
# * lib/beacon_web/controllers/error/404.html.heex
# * lib/beacon_web/controllers/error/500.html.heex
#
# embed_templates "error/*"
def render(template, assigns) do
{_, _, %{extra: %{session: %{"beacon_site" => site}}}} = assigns.conn.private.phoenix_live_view

status =
template
|> String.split(".")
|> hd()
|> String.to_integer()

%{layout: %{template: layout_template}, template: page_template} =
site
|> Beacon.Content.get_error_page(status)
|> Beacon.Repo.preload(:layout)

# The default is to render a plain text page based on
# the template name. For example, "404.html" becomes
# "Not Found".
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
EEx.eval_string(layout_template, assigns: [inner_content: page_template])
end
APB9785 marked this conversation as resolved.
Show resolved Hide resolved
end
32 changes: 32 additions & 0 deletions lib/mix/tasks/install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ defmodule Mix.Tasks.Beacon.Install do

config_file_path = config_file_path("config.exs")
maybe_inject_beacon_repo_into_ecto_repos(config_file_path)
inject_endpoint_render_errors_config(config_file_path)

dev_config_file = config_file_path("dev.exs")
maybe_inject_beacon_repo_config(dev_config_file, bindings)
Expand Down Expand Up @@ -91,6 +92,37 @@ defmodule Mix.Tasks.Beacon.Install do
end
end

@doc false
def inject_endpoint_render_errors_config(config_file_path) do
leandrocp marked this conversation as resolved.
Show resolved Hide resolved
config_file_content = File.read!(config_file_path)

if String.contains?(config_file_content, "BeaconWeb.ErrorHTML") do
Mix.shell().info([
:yellow,
"* skip ",
:reset,
"injecting Beacon.ErrorHTML to render_errors into ",
Path.relative_to_cwd(config_file_path),
" (already exists)"
])
else
regex = ~r/(config.*\.Endpoint,\n)((?:.+\n)*\s*)\n/
render_errors_value = [formats: [html: BeaconWeb.ErrorHTML]]

[_header, endpoint_config_str] = Regex.run(regex, config_file_content, capture: :all_but_first)
{config_list, []} = Code.eval_string("[" <> endpoint_config_str <> "]")
updated_config_list = Keyword.update(config_list, :render_errors, render_errors_value, fn _ -> render_errors_value end)
updated_str = inspect(updated_config_list) <> "\n"

new_config_file_content =
regex
|> Regex.replace(config_file_content, "\\1#{updated_str}")
|> Code.format_string!(file: config_file_path)

File.write!(config_file_path, [new_config_file_content, "\n"])
end
end

@doc false
def maybe_inject_beacon_repo_config(config_file_path, bindings) do
config_file_content = File.read!(config_file_path)
Expand Down
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ defmodule Beacon.MixProject do
Content: [
Beacon.Content,
Beacon.Content.Component,
Beacon.Content.ErrorPage,
Beacon.Content.Layout,
Beacon.Content.LayoutEvent,
Beacon.Content.LayoutSnapshot,
Expand Down Expand Up @@ -171,7 +172,8 @@ defmodule Beacon.MixProject do
"Functions: Stylesheets": &(&1[:type] == :stylesheets),
"Functions: Components": &(&1[:type] == :components),
"Functions: Snippets": &(&1[:type] == :snippets),
"Functions: Page Event Handlers": &(&1[:type] == :page_event_handlers)
"Functions: Page Event Handlers": &(&1[:type] == :page_event_handlers),
"Functions: Error Pages": &(&1[:type] == :error_pages)
]
]
end
Expand Down
17 changes: 17 additions & 0 deletions priv/repo/migrations/20230816195326_create_error_pages.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Beacon.Repo.Migrations.CreateErrorPages do
use Ecto.Migration

def change do
create table(:beacon_error_pages, primary_key: false) do
add :id, :binary_id, primary_key: true
add :site, :text, null: false
add :status, :integer, null: false
add :template, :text, null: false
add :layout_id, references(:beacon_layouts, type: :binary_id)

timestamps()
end

create unique_index(:beacon_error_pages, [:status, :site])
end
end
Loading