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

Allow users to render content in the <head> #457

Merged
merged 2 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let Hooks = {
let socketPath = document.querySelector("html").getAttribute("phx-socket") || "/live"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveView.LiveSocket(socketPath, Phoenix.Socket, {
hooks: Hooks,
hooks: { ...Hooks, ...window.LiveDashboard.customHooks },
params: (liveViewName) => {
return {
_csrf_token: csrfToken,
Expand Down
11,909 changes: 3 additions & 11,906 deletions dist/css/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/app.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/js/app.js.map

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions lib/phoenix/live_dashboard/layout_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,20 @@ defmodule Phoenix.LiveDashboard.LayoutView do
)
end
end

defp custom_head_tags(assigns, key) do
case assigns do
%{^key => components} when is_list(components) ->
assigns = assign(assigns, :components, components)

~H"""
<%= for component <- @components do %>
<%= component.(assigns) %>
<% end %>
"""

_ ->
nil
end
end
end
10 changes: 10 additions & 0 deletions lib/phoenix/live_dashboard/layouts/dash.html.heex
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
<!DOCTYPE html>
<html lang="en" phx-socket={live_socket_path(@conn)}>
<head>
<script nonce={csp_nonce(@conn, :script)}>
window.LiveDashboard = {
customHooks: {},
registerCustomHooks(hooks) {
this.customHooks = {...this.customHooks, ...hooks}
}
}
</script>
<%= custom_head_tags(assigns, :after_opening_head_tag) %>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no, user-scalable=no"/>
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
<title><%= assigns[:page_title] || "Phoenix LiveDashboard" %></title>
<link rel="stylesheet" nonce={csp_nonce(@conn, :style)} href={asset_path(@conn, :css)}>
<script nonce={csp_nonce(@conn, :script)} src={asset_path(@conn, :js)} defer></script>
<%= custom_head_tags(assigns, :before_closing_head_tag) %>
</head>
<body>
<div class="d-flex flex-column align-items-stretch layout-wrapper">
Expand Down
104 changes: 103 additions & 1 deletion lib/phoenix/live_dashboard/page_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ defmodule Phoenix.LiveDashboard.PageBuilder do

We currently support `card/1`, `fields_card/1`, `row/1`,
`shared_usage_card/1`, and `usage_card/1`;
and the live components `live_layered_graph/1`, `live_nav_bar/1`,
and the live components `live_layered_graph/1`, `live_nav_bar/1`,
and `live_table/1`.

## Helpers
Expand All @@ -105,6 +105,84 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
helpers are: `live_dashboard_path/2`, `live_dashboard_path/3`,
`encode_app/1`, `encode_ets/1`, `encode_pid/1`, `encode_port/1`,
and `encode_socket/1`.

## Custom Hooks

If your page needs to register custom hooks, you can use the `register_after_opening_head_tag/2`
function. Because the hooks need to be available on the dead render in the layout, before the
LiveView's LiveSocket is configured, your need to do this inside an `on_mount` hook:

```elixir
defmodule MyAppWeb.MyLiveDashboardHooks do
import Phoenix.LiveView
import Phoenix.Component

alias Phoenix.LiveDashboard.PageBuilder

def on_mount(:default, _params, _session, socket) do
{:cont, PageBuilder.register_after_opening_head_tag(socket, &after_opening_head_tag/1)}
SteffenDE marked this conversation as resolved.
Show resolved Hide resolved
end

defp after_opening_head_tag(assigns) do
~H\"\"\"
<script nonce={@csp_nonces[:script]}>
window.LiveDashboard.registerCustomHooks({
MyHook: {
mounted() {
// do something
}
}
})
</script>
\"\"\"
end
end

defmodule MyAppWeb.MyCustomPage do
...
end
```

And then add it to the list of `on_mount` hooks in the `live_dashboard` router configuration:

```elixir
live_dashboard "/dashboard",
additional_pages: [
route_name: MyAppWeb.MyCustomPage
],
on_mount: [
MyAppWeb.MyLiveDashboardHooks
]
```

The LiveDashboard provides a function `window.LiveDashboard.registerCustomHooks({ ... })` that you can call
with an object of hook declarations.

Note that in order to use external libraries, you will either need to include them from
a CDN, or bundle them yourself and include them from your app's static paths.

> #### A note on CSPs and libraries {: .info}
>
> Phoenix LiveDashboard supports CSP nonces for its own assets, configurable using the
> `Phoenix.LiveDashboard.Router.live_dashboard/2` macro by setting the `:csp_nonce_assign_key`
> option. If you are building a library, ensure that you render those CSP nonces on any scripts,
> styles or images of your page. The nonces are passed to your custom page under the `:csp_nonces` assign
> and also available in the `after_opening_head_tag` component.
>
> You should use those when including scripts or styles like this:
>
> ```heex
> <script nonce={@csp_nonces[:script]}>...</script>
> <script nonce={@csp_nonces[:script]} src="..."></script>
> <style nonce={@csp_nonces[:style]}>...</style>
> <link rel="stylesheet" href="..." nonce={@csp_nonces[:style]}>
> ```
>
> This ensures that your custom page can be used when a CSP is in place using the mechanism
> supported by Phoenix LiveDashboard.
>
> If your custom page needs a different CSP policy, for example due to inline styles set by scripts,
> please consider documenting these requirements.
"""

use Phoenix.Component
Expand Down Expand Up @@ -971,6 +1049,30 @@ defmodule Phoenix.LiveDashboard.PageBuilder do
live_dashboard_path(socket, route, node, old_params, new_params)
end

@doc """
Registers a component to be rendered after the opening head tag in the layout.
"""
def register_after_opening_head_tag(socket, component) do
register_head(socket, component, :after_opening_head_tag)
end

@doc """
Registers a component to be rendered before the closing head tag in the layout.
"""
def register_before_closing_head_tag(socket, component) do
register_head(socket, component, :before_closing_head_tag)
end

defp register_head(socket, component, assign) do
case socket do
%{assigns: %{^assign => [_ | _]}} ->
update(socket, assign, fn existing -> [component | existing] end)

_ ->
assign(socket, assign, [component])
end
end

# TODO: Remove this and the conditional on Phoenix v1.7+
@compile {:no_warn_undefined, Phoenix.VerifiedRoutes}

Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ defmodule Phoenix.LiveDashboard.MixProject do
{:stream_data, "~> 0.1", only: :test},
{:ecto_sqlite3, "~> 0.9.1", only: [:dev, :test]},
{:ex_doc, "~> 0.21", only: :docs},
{:makeup_eex, ">= 0.1.1", only: :docs},
{:esbuild, "~> 0.5", only: :dev},
{:dart_sass, "~> 0.7", only: :dev}
]
Expand Down
6 changes: 4 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_eex": {:hex, :makeup_eex, "1.0.0", "436d4c00204c250b17a775d64e197798aaf374627e6a4f2d3fd3074a8db61db4", [:mix], [{:makeup, "~> 1.2.1 or ~> 1.3", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0 or ~> 1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "3bb699bc519e4f509f1bf8a2e0ba0e08429edf3580053cd31a4f9c1bc5da86c8"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"makeup_html": {:hex, :makeup_html, "0.1.2", "19d4050c0978a4f1618ffe43054c0049f91fe5feeb9ae8d845b5dc79c6008ae5", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b7fb9afedd617d167e6644a0430e49c1279764bfd3153da716d4d2459b0998c5"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"myxql": {:hex, :myxql, "0.6.3", "3d77683a09f1227abb8b73d66b275262235c5cae68182f0cfa5897d72a03700e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "af9eb517ddaced5c5c28e8749015493757fd4413f2cfccea449c466d405d9f51"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
Expand Down
Loading