Current section

Files

Jump to
phoenix_live_dashboard lib phoenix live_dashboard page_live.ex
Raw

lib/phoenix/live_dashboard/page_live.ex

defmodule Phoenix.LiveDashboard.PageNotFound do
@moduledoc false
defexception [:message, plug_status: 404]
end
defmodule Phoenix.LiveDashboard.PageLive do
@moduledoc false
use Phoenix.LiveDashboard.Web, :live_view
import Phoenix.LiveDashboard.Helpers
alias Phoenix.LiveView.Socket
alias Phoenix.LiveDashboard.PageBuilder
alias __MODULE__
@derive {Inspect, only: []}
@default_refresh 15
@refresh_options [1, 2, 5, 15, 30]
defstruct links: [],
nodes: [],
dashboard_mount_path: nil,
refresher?: true,
refresh: @default_refresh,
refresh_options: for(i <- @refresh_options, do: {"#{i}s", i}),
timer: nil
@impl true
def mount(%{"page" => page} = params, %{"pages" => pages} = session, socket) do
case Enum.find(pages, :error, fn {key, _} -> Atom.to_string(key) == page end) do
{_id, {module, page_session}} ->
assign_mount(socket, module, pages, page_session, params, session)
:error ->
raise Phoenix.LiveDashboard.PageNotFound, "unknown page #{inspect(page)}"
end
end
def mount(_params, _session, socket) do
{:ok, redirect_to_current_node(socket)}
end
defp assign_mount(socket, module, pages, page_session, params, session) do
%{
"requirements" => requirements,
"allow_destructive_actions" => allow_destructive_actions,
"csp_nonces" => csp_nonces
} = session
page = %PageBuilder{module: module, allow_destructive_actions: allow_destructive_actions}
socket = assign(socket, page: page, menu: %PageLive{}, csp_nonces: csp_nonces)
with %Socket{redirected: nil} = socket <- assign_params(socket, params),
%Socket{redirected: nil} = socket <- assign_node(socket, params),
%Socket{redirected: nil} = socket <- assign_refresh(socket),
%Socket{redirected: nil} = socket <- assign_menu_links(socket, pages, requirements) do
socket
|> init_schedule_refresh()
|> maybe_apply_module(:mount, [params, page_session], &{:ok, &1})
else
%Socket{} = redirected_socket -> {:ok, redirected_socket}
end
end
defp assign_params(socket, params) do
update_page(socket, params: params, info: params["info"], route: route(params))
end
defp route(%{"page" => page}), do: String.to_existing_atom(page)
defp assign_node(socket, params) do
found_node =
if param_node = params["node"] do
Enum.find(nodes(), &(Atom.to_string(&1) == param_node))
else
node()
end
if found_node do
if connected?(socket) do
:net_kernel.monitor_nodes(true, node_type: :all)
end
socket
|> update_page(node: found_node)
|> update_menu(nodes: nodes())
else
redirect_to_current_node(socket)
end
end
defp assign_refresh(socket) do
module = socket.assigns.page.module
refresh = get_stored_refresh(socket)
update_menu(socket, refresh: refresh, refresher?: module.__page_live__(:refresher?))
end
defp get_stored_refresh(socket) do
key = Atom.to_string(socket.assigns.page.route)
refresh = get_connect_params(socket)["refresh_data"][key]
with false <- is_nil(refresh),
{refresh, ""} <- Integer.parse(refresh),
true <- refresh in @refresh_options do
refresh
else
_ -> @default_refresh
end
end
defp init_schedule_refresh(socket) do
if connected?(socket) and socket.assigns.menu.refresher? do
schedule_refresh(socket)
else
socket
end
end
defp schedule_refresh(socket) do
update_menu(socket,
timer: Process.send_after(self(), :refresh, socket.assigns.menu.refresh * 1000)
)
end
defp assign_menu_links(socket, pages, requirements) do
node = socket.assigns.page.node
capabilities = Phoenix.LiveDashboard.SystemInfo.node_capabilities(node, requirements)
current_route = socket.assigns.page.route |> Atom.to_string()
{links, socket} =
Enum.map_reduce(pages, socket, fn {route, {module, session}}, socket ->
current? = Atom.to_string(route) == current_route
menu_link = module.menu_link(session, capabilities)
case {current?, menu_link} do
{true, {:ok, anchor}} ->
{{:current, anchor}, socket}
{true, _} ->
{:skip, redirect_to_current_node(socket)}
{false, {:ok, anchor}} ->
{{:enabled, anchor, route}, socket}
{false, :skip} ->
{:skip, socket}
{false, {:disabled, anchor}} ->
{{:disabled, anchor}, socket}
{false, {:disabled, anchor, more_info_url}} ->
{{:disabled, anchor, more_info_url}, socket}
end
end)
update_menu(socket, links: links)
end
defp maybe_apply_module(socket, fun, params, default) do
if function_exported?(socket.assigns.page.module, fun, length(params) + 1) do
apply(socket.assigns.page.module, fun, params ++ [socket])
else
default.(socket)
end
end
@impl true
def handle_params(params, url, socket) do
socket =
socket
|> assign_params(params)
|> dashboard_mount_path(url, params)
maybe_apply_module(socket, :handle_params, [params, url], &{:noreply, &1})
end
defp dashboard_mount_path(socket, url, params) do
%{path: path} = URI.parse(url)
range = if params["node"], do: 0..-3//1, else: 0..-2//1
mount_path = path |> String.split("/", trim: true) |> Enum.slice(range) |> Enum.join("/")
mount_path = "/" <> mount_path
update_menu(socket, dashboard_mount_path: mount_path)
end
@impl true
def render(assigns) do
~H"""
<header class="d-flex">
<div id="menu" class="container d-flex flex-column">
<h1>Phoenix LiveDashboard</h1>
<div id="nav-dropdowns">
<form id="refresher" phx-change="select_refresh">
<%= if @menu.refresher? do %>
<label for="refresh-interval-select">Update every</label>
<select
name="refresh"
class="custom-select custom-select-sm"
id="refresh-interval-select"
data-page={@page.route}
data-dashboard-mount-path={@menu.dashboard_mount_path}
phx-hook="PhxRememberRefresh"
>
<%= options_for_select(@menu.refresh_options, @menu.refresh) %>
</select>
<% else %>
<label class="no-select">Updates automatically</label>
<% end %>
</form>
<form id="node-selection" phx-change="select_node" phx-auto-recover="ignore">
<label for="node-select">Selected node</label>
<select name="node" class="custom-select custom-select-sm" id="node-select">
<%= options_for_select(@menu.nodes, @page.node) %>
</select>
</form>
</div>
<nav id="menu-bar">
<%= for link <- @menu.links, link != :skip do %>
<%= maybe_link(@socket, @page, link) %>
<% end %>
</nav>
</div>
</header>
<PageBuilder.live_modal
:if={@page.info}
id="modal"
title={@page.info}
return_to={modal_return_to(@socket, @page).(@page.node, [])}
>
<.live_component
id={@page.info}
module={extract_info_component(@page.info)}
path={modal_return_to(@socket, @page)}
return_to={modal_return_to(@socket, @page).(@page.node, [])}
page={@page}
/>
</PageBuilder.live_modal>
<section id="main" role="main" class="container">
<%= render_page(@page.module, assigns) %>
</section>
"""
end
defp render_page(module, assigns) do
module.render(assigns)
end
defp modal_return_to(socket, %{route: route, params: params}) do
params = Map.delete(params, "info")
&PageBuilder.live_dashboard_path(socket, route, &1, params, Enum.into(&2, params))
end
defp extract_info_component("PID<" <> _), do: Phoenix.LiveDashboard.ProcessInfoComponent
defp extract_info_component("Port<" <> _), do: Phoenix.LiveDashboard.PortInfoComponent
defp extract_info_component("Socket<" <> _), do: Phoenix.LiveDashboard.SocketInfoComponent
defp extract_info_component("ETS<" <> _), do: Phoenix.LiveDashboard.EtsInfoComponent
defp extract_info_component("App<" <> _), do: Phoenix.LiveDashboard.AppInfoComponent
defp extract_info_component(_), do: nil
@impl true
def handle_info({:nodeup, _, _}, socket) do
{:noreply, assign(socket, nodes: nodes())}
end
def handle_info({:nodedown, _, _}, socket) do
{:noreply, validate_nodes_or_redirect(socket)}
end
def handle_info(:refresh, socket) do
socket
|> update(:page, fn page -> %{page | tick: page.tick + 1} end)
|> schedule_refresh()
|> maybe_apply_module(:handle_refresh, [], &{:noreply, &1})
end
def handle_info(message, socket) do
maybe_apply_module(socket, :handle_info, [message], &{:noreply, &1})
end
@impl true
def handle_event("select_node", %{"node" => param_node}, socket) do
node = Enum.find(nodes(), &(Atom.to_string(&1) == param_node))
page = socket.assigns.page
if node && node != page.node do
to = PageBuilder.live_dashboard_path(socket, page.route, node, page.params, page.params)
{:noreply, push_navigate(socket, to: to)}
else
{:noreply, redirect_to_current_node(socket)}
end
end
def handle_event("select_refresh", params, socket) do
case Integer.parse(params["refresh"]) do
{refresh, ""} -> {:noreply, update_menu(socket, refresh: refresh)}
_ -> {:noreply, socket}
end
end
def handle_event("show_info", %{"info" => info}, socket) do
to = PageBuilder.live_dashboard_path(socket, socket.assigns.page, info: info)
{:noreply, push_patch(socket, to: to)}
end
def handle_event(event, params, socket) do
socket.assigns.page.module.handle_event(event, params, socket)
end
## Navbar handling
defp maybe_link(_socket, _page, {:current, text}) do
assigns = %{text: text}
~H"""
<div class="menu-item active"><%= @text %></div>
"""
end
defp maybe_link(socket, page, {:enabled, text, route}) do
assigns = %{text: text, route: route, socket: socket, page: page}
~H"""
<.link
navigate={PageBuilder.live_dashboard_path(@socket, @route, @page.node, @page.params, [])}
class="menu-item"
>
<%= @text %>
</.link>
"""
end
defp maybe_link(_socket, _page, {:disabled, text}) do
assigns = %{text: text}
~H"""
<div class="menu-item menu-item-disabled">
<%= @text %>
</div>
"""
end
defp maybe_link(_socket, _page, {:disabled, text, more_info_url}) do
assigns = %{more_info_url: more_info_url, text: text}
~H"""
<div class="menu-item menu-item-disabled">
<%= @text %> <a href={@more_info_url} class="menu-item-enable-button">Enable</a>
</div>
"""
end
## Node helpers
defp validate_nodes_or_redirect(socket) do
if socket.assigns.page.node not in nodes() do
socket
|> put_flash(:error, "Node #{socket.assigns.page.node} disconnected.")
|> redirect_to_current_node()
else
assign(socket, nodes: nodes())
end
end
defp redirect_to_current_node(socket) do
push_navigate(socket, to: PageBuilder.live_dashboard_path(socket, :home, node(), %{}, %{}))
end
defp update_page(socket, assigns) do
update(socket, :page, fn page ->
Enum.reduce(assigns, page, fn {key, value}, page ->
Map.replace!(page, key, value)
end)
end)
end
defp update_menu(socket, assigns) do
update(socket, :menu, fn page ->
Enum.reduce(assigns, page, fn {key, value}, page ->
Map.replace!(page, key, value)
end)
end)
end
end