-
Notifications
You must be signed in to change notification settings - Fork 185
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
Conversation
Relates to: #455
.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off { | ||
display: none; | ||
} /*! | ||
#nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px, -4px);-ms-transform:rotate(3deg) translate(0px, -4px);transform:rotate(3deg) translate(0px, -4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.uplot,.uplot *,.uplot *::before,.uplot *::after{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";line-height:1.5;width:min-content}.u-title{text-align:center;font-size:18px;font-weight:bold}.u-wrap{position:relative;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;position:relative;width:100%;height:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{vertical-align:middle;display:inline-block}.u-legend .u-marker{width:1em;height:1em;margin-right:4px;background-clip:padding-box !important}.u-inline.u-live th::after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:rgba(0, 0, 0, 0.07);position:absolute;pointer-events:none}.u-cursor-x,.u-cursor-y{position:absolute;left:0;top:0;pointer-events:none;will-change:transform;z-index:100}.u-hz .u-cursor-x,.u-vt .u-cursor-y{height:100%;border-right:1px dashed #607d8b}.u-hz .u-cursor-y,.u-vt .u-cursor-x{width:100%;border-bottom:1px dashed #607d8b}.u-cursor-pt{position:absolute;top:0;left:0;border-radius:50%;border:0 solid;pointer-events:none;will-change:transform;z-index:100;background-clip:padding-box !important}.u-axis.u-off,.u-select.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-cursor-pt.u-off{display:none}/*! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what happened here, I just did a mix assets.build
.
Ah and here is an example of a custom page using the new API: Application.put_env(:sample, Example.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Application.put_env(
:phoenix_live_dashboard,
:before_closing_head_tag,
{Example.TerminalPage, :before_closing_head_tag}
)
Mix.install([
{:plug_cowboy, "~> 2.5"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.7"},
{:phoenix_live_view, "~> 1.0.0-rc.7", override: true},
{:phoenix_live_dashboard, github: "phoenixframework/phoenix_live_dashboard", branch: "sd-before_closing_head_tag", override: true},
# {:phoenix_live_dashboard, path: "~/oss/phoenix_live_dashboard", override: true},
{:extty, "~> 0.4.1"}
])
defmodule Example.Hooks do
import Phoenix.LiveView
import Phoenix.Component
def on_mount(:default, _params, _session, socket) do
{:cont, Phoenix.LiveDashboard.PageBuilder.register_after_opening_head_tag(socket, &after_opening_head_tag/1) |> dbg()}
end
defp after_opening_head_tag(assigns) do
~H"""
<script nonce={@csp_nonces[:script]} src="https://unpkg.com/@xterm/[email protected]/lib/xterm.js">
</script>
<script nonce={@csp_nonces[:script]} src="https://unpkg.com/@xterm/[email protected]/lib/addon-fit.js">
</script>
<script nonce={@csp_nonces[:script]}>
window.LiveDashboard.registerCustomHooks({
Terminal: {
init() {
if (this.terminal) this.terminal.dispose();
this.terminal = new Terminal({
theme: this.getTheme(),
});
const fitAddon = new FitAddon.FitAddon();
this.fitAddon = fitAddon;
this.terminal.loadAddon(fitAddon);
this.terminal.open(this.el);
fitAddon.fit();
// send initial size (we need to wait for the first patch)
this.pushEvent("resize", { cols: this.terminal.cols, rows: this.terminal.rows });
this.terminal.onResize((size) => this.pushEvent("resize", { cols: size.cols, rows: size.rows }));
this.terminal.onData((data) => this.pushEvent("terminal", data));
},
mounted() {
this.init();
this.handleEvent("new-terminal", () => this.init());
this.handleEvent("terminal", ({ data }) => {
if (this.terminal) this.terminal.write(data);
});
this.resizeHandler = this.handleResize.bind(this);
window.addEventListener("resize", this.resizeHandler);
},
getTheme() {
return {
background: "#00000000",
foreground: "#6C696E",
selectionBackground: "#6C696E70",
cursor: "#6C696E",
cursorAccent: "#6C696E",
black: "#FFFFFF",
blue: "#775DFF",
brightBlack: "#A7A5A8",
brightBlue: "#775DFF",
brightCyan: "#149BDA",
brightGreen: "#17AD98",
brightMagenta: "#AA17E6",
brightRed: "#D8137F",
brightWhite: "#322D34",
brightYellow: "#DC8A0E",
cyan: "#149BDA",
green: "#17AD98",
magenta: "#AA17E6",
red: "#D8137F",
white: "#6C696E",
yellow: "#DC8A0E"
};
},
handleResize() {
if (this.fitAddon) this.fitAddon.fit();
},
destroyed() {
window.removeEventListener("resize", this.resizeHandler);
}
}
});
</script>
<link rel="stylesheet" href="https://unpkg.com/@xterm/[email protected]/css/xterm.css" nonce={@csp_nonces[:style]} />
"""
end
end
defmodule Example.TerminalPage do
@moduledoc false
use Phoenix.LiveDashboard.PageBuilder, refresher?: false
@impl true
def mount(params, _session, socket) do
tty =
if connected?(socket) and not is_map_key(params, "node") do
{:ok, tty} = ExTTY.start_link(handler: self())
tty
end
socket
|> assign(:tty, tty)
|> assign(:nodes, [:self | Node.list()])
|> assign(:node, :self)
|> then(&{:ok, &1})
end
@impl true
def handle_params(%{"node" => node}, _uri, socket) do
node = String.to_existing_atom(node)
socket
|> assign(:node, node)
|> push_event("new-terminal", %{})
|> connect_tty()
|> then(&{:noreply, &1})
end
def handle_params(_, _, socket), do: {:noreply, socket}
@impl true
def menu_link(_, _) do
{:ok, "IEx"}
end
def handle_event("terminal", data, socket) do
ExTTY.send_text(socket.assigns.tty, data)
{:noreply, socket}
end
def handle_event("resize", %{"rows" => rows, "cols" => cols}, socket) do
ExTTY.window_change(socket.assigns.tty, cols, rows)
{:noreply, socket}
end
defp connect_tty(socket) do
if socket.assigns.tty do
Process.unlink(socket.assigns.tty)
Process.exit(socket.assigns.tty, :normal)
end
opts =
case socket.assigns.node do
:self -> [handler: self()]
node -> [handler: self(), remsh: node]
end
{:ok, tty} = ExTTY.start_link(opts)
assign(socket, tty: tty)
end
@impl true
def handle_info({:tty_data, data}, socket) do
{:noreply, push_event(socket, "terminal", %{"data" => data})}
end
def handle_info(_msg, socket), do: {:noreply, socket}
@impl true
def render(assigns) do
~H"""
<div :if={connected?(@socket)} id="terminal-page" phx-update="ignore" style="height: calc(100vh - 250px); padding: 4px; border: 1px solid #746f97; background: #00000005;">
<div
id="terminal"
phx-hook="Terminal"
style="height: 100%;"
>
</div>
</div>
"""
end
end
defmodule Example.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule Example.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
import Phoenix.LiveDashboard.Router
def put_csp(conn, _opts) do
[img_nonce, script_nonce] =
for _i <- 1..2, do: 16 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false)
conn
|> assign(:img_csp_nonce, img_nonce)
|> assign(:script_csp_nonce, script_nonce)
|> put_resp_header(
"content-security-policy",
"default-src; script-src 'nonce-#{script_nonce}'; style-src 'self' https://unpkg.com 'unsafe-inline'; " <>
"img-src 'nonce-#{img_nonce}' data: ; font-src data: ; connect-src 'self'; frame-src 'self' ;"
)
end
pipeline :browser do
plug(:accepts, ["html"])
plug :put_csp
end
scope "/", Example do
pipe_through(:browser)
live_dashboard "/",
additional_pages: [
terminal: Example.TerminalPage
],
on_mount: Example.Hooks,
csp_nonce_assign_key: %{
img: :img_csp_nonce,
style: :style_csp_nonce,
script: :script_csp_nonce
}
end
end
defmodule Example.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket("/live", Phoenix.LiveView.Socket)
plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"
plug(Example.Router)
end
{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity) |
c60af1d
to
e7728dd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LGTM as long as you think it won't overlap with what we may add to LiveView. :)
Also, it seems you are removing a bunch of files, just keep in mind that we always bundle precompiled assets in case someone wants to use the repo from git :)
awesome job, ship it!!!
It won't. Colocated hooks will (maybe) make this easier, so this feature won't be needed for bringing hooks to custom pages any more, but it also wouldn't have any conflicts.
The removals are from the changes to the compiled app.css. Previously it wasn't minified into one line, now it is, but I don't know why.
Thank you! 🚀 |
Thank you! Can you please push a new version out? |
I don’t have the permissions to, but @josevalim can :) |
@egze please give it a try in your custom page and once it works, let us know, and I can ship it. Just in case there is something pending/missing/incorrect while you work on it. |
@josevalim works like a charm |
Released. |
Relates to: #455
@josevalim instead of storing the components in application env or in a process, I thought about it a little bit more and decided to use assigns together with the already possible
on_mount
option to define hooks running on all LiveViews.Recap: it is currently not possible to create custom dashboard pages that need their own
phx-hook
s. In order to make this work, the hooks must be available when constructing the LiveSocket, therefore we need a way to run custom scripts before the live dashboard app.js runs. It is also important that they run on all pages as they are only executed on the dead render and not later live navigations.I want to revisit this when we get to colocated hooks, as they will possibly allow this to work with less configuration.
I didn't create an issue in LiveView yet, as even if we had support for executing
<script>
tags dynamically added, we still would not be able to use those to add hooks, as LiveView does not support adding hooks after the LiveSocket was already created. See phoenixframework/phoenix_live_view#2744 and phoenixframework/phoenix_live_view#2563.