defmodule Oban.Web.DashboardLive do use Oban.Web, :live_view alias Oban.Web.{CronsPage, JobsPage, QueuesPage, WorkflowsPage} @impl Phoenix.LiveView def mount(params, session, socket) do %{"prefix" => prefix, "resolver" => resolver} = session %{"live_path" => live_path, "live_transport" => live_transport} = session %{"user" => user, "access" => access, "csp_nonces" => csp_nonces} = session %{"refresh" => refresh, "logo_path" => logo_path} = session refresh = restore_state(socket, "refresh", refresh) theme = restore_state(socket, "theme", "system") sidebar_width = restore_state(socket, "sidebar_width", 320) oban = current_oban_instance(session, socket) conf = await_init([oban]) _met = await_init([oban, Oban.Met]) page = resolve_page(params) Process.put(:routing, {socket, prefix}) Process.put(:csp_nonce_style, csp_nonces.style) socket = socket |> assign(conf: conf, params: params, page: page, init_state: init_state(socket)) |> assign(live_path: live_path, live_transport: live_transport, logo_path: logo_path) |> assign(access: access, csp_nonces: csp_nonces, resolver: resolver, user: user) |> assign(original_refresh: nil, refresh: refresh, timer: nil, theme: theme) |> assign(sidebar_width: sidebar_width) |> init_schedule_refresh() |> page.comp.handle_mount() {:ok, socket} end defp current_oban_instance(session, socket) do stashed = restore_state(socket, "instance") default = List.first(oban_instances()) case stashed || session["oban"] || default || Oban do name when is_binary(name) -> name |> String.split(".") |> Module.safe_concat() name -> name end end defp init_state(socket) do case get_connect_params(socket) do %{"init_state" => state} -> state _ -> %{} end end defp restore_state(socket, key, default \\ nil) do socket |> init_state() |> Map.get("oban:" <> key, default) end @impl Phoenix.LiveView def render(assigns) do assigns = assigns |> Map.put(:id, "page") |> Map.drop(~w(flash live_path live_transport refresh socket timer)a) ~H""" <.live_component id="page" module={@page.comp} {assigns} /> """ end @impl Phoenix.LiveView def terminate(_reason, %{assigns: %{timer: timer}}) do if is_reference(timer), do: Process.cancel_timer(timer) :ok end def terminate(_reason, _socket), do: :ok @impl Phoenix.LiveView def handle_params(params, uri, socket) do page = resolve_page(params) if page == socket.assigns.page do page.comp.handle_params(params, uri, socket) else socket = socket |> assign(params: params, page: page) |> page.comp.handle_mount() page.comp.handle_params(params, uri, socket) end end @impl Phoenix.LiveView def handle_info(:clear_flash, socket) do {:noreply, clear_flash(socket)} end def handle_info(:pause_refresh, socket) do socket = if socket.assigns.refresh > 0 do cancel_timer(socket) assign(socket, refresh: -1, original_refresh: socket.assigns.refresh, timer: nil) else socket end {:noreply, socket} end def handle_info(:resume_refresh, socket) do if original = socket.assigns.original_refresh do handle_info({:update_refresh, original}, socket) else {:noreply, socket} end end def handle_info({:select_instance, name}, socket) do name = name |> String.split(".") |> Module.safe_concat() conf = Oban.config(name) name = inspect(name) page = socket.assigns.page socket = socket |> assign(oban: name, conf: conf) |> push_event("select-instance", %{name: name}) |> put_flash_with_clear(:info, "Switched instance to #{name}") |> page.comp.handle_refresh() {:noreply, socket} end def handle_info(:toggle_refresh, socket) do %{original_refresh: original, refresh: refresh} = socket.assigns cond do refresh > 0 -> socket = socket |> assign(refresh: -1, original_refresh: refresh) |> push_event("update-refresh", %{refresh: -1}) {:noreply, socket} is_integer(original) -> handle_info({:update_refresh, original}, socket) true -> handle_info({:update_refresh, 1}, socket) end end def handle_info({:update_refresh, refresh}, socket) do socket = socket |> assign(refresh: refresh, original_refresh: nil) |> schedule_refresh() {:noreply, push_event(socket, "update-refresh", %{refresh: refresh})} end def handle_info({:update_theme, theme}, socket) do {:noreply, socket |> assign(theme: theme) |> push_event("update-theme", %{theme: theme})} end def handle_info(:refresh, socket) do socket = socket |> socket.assigns.page.comp.handle_refresh() |> schedule_refresh() {:noreply, socket} end def handle_info(message, socket) do socket.assigns.page.comp.handle_info(message, socket) end @impl Phoenix.LiveView def handle_event("sidebar_resize", %{"width" => width}, socket) do {:noreply, assign(socket, sidebar_width: width)} end def handle_event(event, params, socket) do socket.assigns.page.comp.handle_event(event, params, socket) end ## Mount Helpers defp await_init(args, opts \\ []) defp await_init([oban_name | _] = args, opts) do retries = Keyword.get(opts, :retries, 150) timeout = Keyword.get(opts, :timeout, 100) case apply(Oban.Registry, :whereis, args) do nil when retries > 0 -> Process.sleep(timeout) await_init(args, Keyword.put(opts, :retries, retries - 1)) nil -> raise RuntimeError, "no config registered for #{inspect(args)} instance" pid when is_pid(pid) -> Oban.config(oban_name) end end ## Render Helpers defp resolve_page(%{"page" => "jobs"}), do: %{name: :jobs, comp: JobsPage} defp resolve_page(%{"page" => "queues"}), do: %{name: :queues, comp: QueuesPage} defp resolve_page(%{"page" => "crons"}), do: %{name: :crons, comp: CronsPage} defp resolve_page(%{"page" => "workflows"}), do: %{name: :workflows, comp: WorkflowsPage} defp resolve_page(_params), do: %{name: :jobs, comp: JobsPage} ## Refresh Helpers defp init_schedule_refresh(socket) do if connected?(socket) do schedule_refresh(socket) else assign(socket, timer: nil) end end defp schedule_refresh(socket) do cancel_timer(socket) if socket.assigns.refresh > 0 do interval = :timer.seconds(socket.assigns.refresh) - 50 assign(socket, timer: Process.send_after(self(), :refresh, interval)) else assign(socket, timer: nil) end end defp cancel_timer(socket) do if is_reference(socket.assigns.timer), do: Process.cancel_timer(socket.assigns.timer) end end