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"""