├── .formatter.exs ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── config └── config.exs ├── doc └── images │ ├── obanalyze.png │ └── obanalyze_job.png ├── lib ├── obanalyze.ex └── obanalyze │ ├── dashboard.ex │ ├── helpers.ex │ ├── hooks.ex │ ├── nav_item.ex │ └── oban_jobs.ex ├── mix.exs ├── mix.lock └── test ├── obanalyze └── dashboard_test.exs ├── obanalyze_test.exs ├── support └── migrations │ └── 20241110100000_add_oban_jobs_table.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :ecto_sql, :phoenix], 3 | subdirectories: ["priv/*/migrations"], 4 | plugins: [Phoenix.LiveView.HTMLFormatter], 5 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"], 6 | heex_line_length: 300 7 | ] 8 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Elixir CI 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | pull_request: 12 | branches: ["main"] 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | name: Build and test 20 | runs-on: ubuntu-20.04 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Elixir 25 | uses: erlef/setup-beam@v1 26 | with: 27 | elixir-version: "1.17.3" # [Required] Define the Elixir version 28 | otp-version: "27.1" # [Required] Define the Erlang/OTP version 29 | - name: Restore dependencies cache 30 | uses: actions/cache@v3 31 | with: 32 | path: deps 33 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 34 | restore-keys: ${{ runner.os }}-mix- 35 | - name: Install dependencies 36 | run: mix deps.get 37 | - name: Run tests 38 | run: mix test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | *.beam 9 | /config/*.secret.exs 10 | .elixir_ls/ 11 | /.fetch 12 | *.tar 13 | /tmp/ 14 | *.db* 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.4.2 4 | 5 | * Support custom named Oban instances. 6 | 7 | ## v1.4.1 8 | 9 | * Fix filtering in Postgres for `Smart` engine. 10 | 11 | ## v1.4.0 12 | 13 | * Fix filtering in Postgres. 14 | 15 | ## v1.3.1 16 | 17 | * Improved README, no new features. 18 | 19 | ## v1.3.0 20 | 21 | * Add search. 22 | 23 | ## v1.2.0 24 | 25 | * Ability to retry, cancel, delete jobs from the modal. 26 | 27 | ## v1.1.1 28 | 29 | * Update docs to mention `on_mount` hooks. 30 | 31 | ## v1.1.0 32 | 33 | * Bump `phoenix_live_dashboard` to `>= 0.8.5`. 34 | * Report timestamps with `Intl.RelativeTimeFormat` in frontend. 35 | 36 | ## v1.0.0 37 | 38 | * Basic dashboard 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 Aleksandr Lossenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obanalyze 2 | 3 | 4 | 5 | Obanalyze provides real-time monitoring for `Oban` within `Phoenix.LiveDashboard`, 6 | delivering a user-friendly interface to manage background jobs seamlessly. 7 | 8 | ## Features 9 | 10 | - **Job Management**: Retry, cancel, or delete jobs directly from the dashboard. 11 | - **Filtering**: Filter jobs based on worker name or job arguments to quickly find what you're looking for. 12 | - **Database Compatibility**: Fully compatible with SQLite, PostgreSQL or any database that Oban uses. 13 | 14 | ## Installation 15 | 16 | The package can be installed by adding `obanalyze` to your list of dependencies in `mix.exs`: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | {:obanalyze, "~> 1.4"} 22 | ] 23 | end 24 | ``` 25 | 26 | ## Configuration 27 | 28 | Configure your application to include Obanalyze in `Phoenix.LiveDashboard`. 29 | Update your router configuration as follows: 30 | 31 | ```elixir 32 | # lib/my_app_web/router.ex 33 | live_dashboard "/dashboard", 34 | additional_pages: [ 35 | obanalyze: Obanalyze.dashboard() 36 | ], 37 | on_mount: [ 38 | Obanalyze.hooks() 39 | ] 40 | ``` 41 | 42 | ## Usage 43 | 44 | After installation and setup, navigate to your `Phoenix.LiveDashboard` at the specified 45 | route (e.g., `/dev/dashboard`). You will see the new `Obanalyze` tab, which provides 46 | a complete overview of your background jobs. 47 | 48 | ## List view 49 | 50 | ![Obanalyze screenshot](doc/images/obanalyze.png "Obanalyze") 51 | 52 | ## Single job view 53 | 54 | ![Obanalyze job screenshot](doc/images/obanalyze_job.png "Single Job") 55 | 56 | 57 | # Alternatives 58 | 59 | - [evilmarty/oban_live_dashboard](https://github.com/evilmarty/oban_live_dashboard): Inspired this project; a simplistic approach to Oban job monitoring. 60 | - [Oban Web](https://getoban.pro): Official advanced dashboard offering extensive features, directly from the creators of Oban. 61 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :warning 4 | -------------------------------------------------------------------------------- /doc/images/obanalyze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egze/obanalyze/a9f22fec2769c777bfb9c2d6949a70eb7053908f/doc/images/obanalyze.png -------------------------------------------------------------------------------- /doc/images/obanalyze_job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egze/obanalyze/a9f22fec2769c777bfb9c2d6949a70eb7053908f/doc/images/obanalyze_job.png -------------------------------------------------------------------------------- /lib/obanalyze.ex: -------------------------------------------------------------------------------- 1 | defmodule Obanalyze do 2 | @external_resource Path.expand("./README.md") 3 | @moduledoc File.read!(Path.expand("./README.md")) 4 | |> String.split("") 5 | |> Enum.fetch!(1) 6 | |> String.replace("doc/images", "images") 7 | 8 | @doc """ 9 | Returns the module for the Obanalyze Phoenix.LiveDashboard page. 10 | """ 11 | def dashboard do 12 | Obanalyze.Dashboard 13 | end 14 | 15 | @doc """ 16 | Returns the module for the Obanalyze JS hooks config. 17 | """ 18 | def hooks do 19 | Obanalyze.Hooks 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/obanalyze/dashboard.ex: -------------------------------------------------------------------------------- 1 | defmodule Obanalyze.Dashboard do 2 | @moduledoc false 3 | 4 | use Phoenix.LiveDashboard.PageBuilder, refresher?: true 5 | 6 | import Phoenix.LiveDashboard.Helpers, only: [format_value: 2] 7 | import Obanalyze.Helpers 8 | 9 | alias Obanalyze.ObanJobs 10 | alias Obanalyze.NavItem 11 | 12 | @per_page_limits [20, 50, 100] 13 | 14 | @impl true 15 | def render(assigns) do 16 | ~H""" 17 | 22 | 23 |

Obanalyze

24 | 25 |

Filter jobs by state:

26 | 27 | <.live_nav_bar id="oban_states" page={@page} nav_param="job_state" style={:bar} extra_params={["nav"]}> 28 | <:item :for={nav_item <- @nav_items} name={nav_item.name} label={nav_item.label} method="navigate"> 29 | <.live_table id="oban_jobs" limit={per_page_limits()} dom_id={"oban-jobs-#{nav_item.name}"} page={@page} row_attrs={&row_attrs/1} row_fetcher={&row_fetcher(&1, &2, nav_item.name)} default_sort_by={@default_sort_by} title="" search={true}> 30 | <:col field={:id} sortable={:desc} /> 31 | <:col :let={job} field={:worker} sortable={:desc}> 32 |

<%= job.worker %>

33 |
<%= truncate(inspect(job.args)) %>
34 | 35 | <:col :let={job} field={:attempt} header="Attempt" sortable={:desc}> 36 | <%= job.attempt %>/<%= job.max_attempts %> 37 | 38 | <:col field={:queue} header="Queue" sortable={:desc} /> 39 | <:col :let={job} field={nav_item.timestamp_field} sortable={nav_item.default_timestamp_field_sort}> 40 | <%= format_value(timestamp(job, nav_item.timestamp_field)) %> 41 | 42 | 43 | 44 | 45 | 46 | <.live_modal :if={@job != nil} id="job-modal" title={"Job - #{@job.id}"} return_to={live_dashboard_path(@socket, @page, params: %{})}> 47 | 61 |
62 | <.label_value_list> 63 | <:elem label="ID"><%= @job.id %> 64 | <:elem label="State"><%= @job.state %> 65 | <:elem label="Queue"><%= @job.queue %> 66 | <:elem label="Worker"><%= @job.worker %> 67 | <:elem label="Args"><%= format_value(@job.args, nil) %> 68 | <:elem :if={@job.meta != %{}} label="Meta"><%= format_value(@job.meta, nil) %> 69 | <:elem :if={@job.tags != []} label="Tags"><%= format_value(@job.tags, nil) %> 70 | <:elem :if={@job.errors != []} label="Errors"><%= format_errors(@job.errors) %> 71 | <:elem label="Attempts"><%= @job.attempt %>/<%= @job.max_attempts %> 72 | <:elem label="Priority"><%= @job.priority %> 73 | <:elem label="Attempted at"><%= format_value(@job.attempted_at) %> 74 | <:elem :if={@job.cancelled_at} label="Cancelled at"><%= format_value(@job.cancelled_at) %> 75 | <:elem :if={@job.completed_at} label="Completed at"><%= format_value(@job.completed_at) %> 76 | <:elem :if={@job.discarded_at} label="Discarded at"><%= format_value(@job.discarded_at) %> 77 | <:elem label="Inserted at"><%= format_value(@job.inserted_at) %> 78 | <:elem label="Scheduled at"><%= format_value(@job.scheduled_at) %> 79 | 80 |
81 | 82 | """ 83 | end 84 | 85 | @impl true 86 | def mount(_params, _, socket) do 87 | {:ok, socket} 88 | end 89 | 90 | @impl true 91 | def menu_link(_, _) do 92 | {:ok, "Obanalyze"} 93 | end 94 | 95 | @impl true 96 | def handle_params(params, _uri, socket) do 97 | socket = 98 | socket 99 | |> assign_nav_items() 100 | |> assign_default_sort_by(params["job_state"]) 101 | |> assign_job(get_in(params, ["params", "job"])) 102 | 103 | {:noreply, socket} 104 | end 105 | 106 | @impl true 107 | def handle_event("show_job", params, socket) do 108 | to = live_dashboard_path(socket, socket.assigns.page, params: params) 109 | {:noreply, push_patch(socket, to: to)} 110 | end 111 | 112 | def handle_event("cancel_job", %{"job" => job_id}, socket) do 113 | with {:ok, job} <- ObanJobs.cancel_oban_job(job_id) do 114 | {:noreply, assign(socket, job: job)} 115 | end 116 | end 117 | 118 | def handle_event("retry_job", %{"job" => job_id}, socket) do 119 | with {:ok, job} <- ObanJobs.retry_oban_job(job_id) do 120 | {:noreply, assign(socket, job: job)} 121 | end 122 | end 123 | 124 | def handle_event("delete_job", %{"job" => job_id}, socket) do 125 | with :ok <- ObanJobs.delete_oban_job(job_id) do 126 | to = live_dashboard_path(socket, socket.assigns.page, params: %{}) 127 | {:noreply, push_patch(socket, to: to)} 128 | end 129 | end 130 | 131 | @impl true 132 | def handle_refresh(socket) do 133 | socket = 134 | socket 135 | |> assign_nav_items() 136 | |> update(:job, fn 137 | %Oban.Job{id: job_id} -> ObanJobs.get_oban_job(job_id) 138 | _ -> nil 139 | end) 140 | 141 | {:noreply, socket} 142 | end 143 | 144 | defp assign_job(socket, job_id) do 145 | if job_id do 146 | case ObanJobs.fetch_oban_job(job_id) do 147 | {:ok, job} -> 148 | assign(socket, job: job) 149 | 150 | _ -> 151 | to = live_dashboard_path(socket, socket.assigns.page, params: %{}) 152 | push_patch(socket, to: to) 153 | end 154 | else 155 | assign(socket, job: nil) 156 | end 157 | end 158 | 159 | defp assign_nav_items(socket) do 160 | assign(socket, nav_items: get_nav_items()) 161 | end 162 | 163 | defp assign_default_sort_by(socket, job_state) do 164 | timestamp_field = ObanJobs.timestamp_field_for_job_state(job_state) 165 | 166 | assign(socket, :default_sort_by, timestamp_field) 167 | end 168 | 169 | defp row_fetcher(params, _node, job_state) do 170 | ObanJobs.list_jobs_with_total(params, job_state) 171 | end 172 | 173 | defp row_attrs(job) do 174 | [ 175 | {"phx-click", "show_job"}, 176 | {"phx-value-job", job[:id]}, 177 | {"phx-page-loading", true} 178 | ] 179 | end 180 | 181 | defp format_errors(errors) do 182 | Enum.map(errors, &Map.get(&1, "error")) 183 | end 184 | 185 | defp format_value(%DateTime{} = datetime) do 186 | Calendar.strftime(datetime, "%Y-%m-%d %H:%M:%S") 187 | end 188 | 189 | defp format_value(value), do: value 190 | 191 | defp timestamp(job, timestamp_field) do 192 | Map.get(job, timestamp_field) 193 | end 194 | 195 | defp truncate(string, max_length \\ 50) do 196 | if String.length(string) > max_length do 197 | String.slice(string, 0, max_length) <> "…" 198 | else 199 | string 200 | end 201 | end 202 | 203 | defp per_page_limits, do: @per_page_limits 204 | 205 | @doc """ 206 | Returns the nav items to render the menu. 207 | """ 208 | def get_nav_items do 209 | job_states_with_count = ObanJobs.job_states_with_count() 210 | 211 | for job_state <- ObanJobs.sorted_job_states(), 212 | count = Map.get(job_states_with_count, job_state, 0), 213 | timestamp_field = ObanJobs.timestamp_field_for_job_state(job_state), 214 | do: NavItem.new(job_state, count, timestamp_field) 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /lib/obanalyze/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Obanalyze.Helpers do 2 | alias Oban.Job 3 | 4 | def can_cancel_job?(%Job{} = job) do 5 | job.state in ["available", "executing", "inserted", "retryable", "scheduled"] 6 | end 7 | 8 | def can_delete_job?(%Job{} = job) do 9 | job.state not in ["executing"] 10 | end 11 | 12 | def can_retry_job?(%Job{} = job) do 13 | job.state in ["cancelled", "completed", "discarded", "retryable"] 14 | end 15 | 16 | def can_run_job?(%Job{} = job) do 17 | job.state in ["inserted", "scheduled"] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/obanalyze/hooks.ex: -------------------------------------------------------------------------------- 1 | defmodule Obanalyze.Hooks do 2 | import Phoenix.Component 3 | 4 | alias Phoenix.LiveDashboard.PageBuilder 5 | 6 | def on_mount(:default, _params, _session, socket) do 7 | {:cont, PageBuilder.register_after_opening_head_tag(socket, &after_opening_head_tag/1)} 8 | end 9 | 10 | defp after_opening_head_tag(assigns) do 11 | ~H""" 12 | 72 | """ 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/obanalyze/nav_item.ex: -------------------------------------------------------------------------------- 1 | defmodule Obanalyze.NavItem do 2 | defstruct [:name, :label, :timestamp_field, :default_timestamp_field_sort] 3 | 4 | def new(state, count, timestamp_field) do 5 | %__MODULE__{ 6 | name: state, 7 | label: "#{Phoenix.Naming.humanize(state)} (#{count})", 8 | timestamp_field: timestamp_field, 9 | default_timestamp_field_sort: if(timestamp_field == :scheduled_at, do: :asc, else: :desc) 10 | } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/obanalyze/oban_jobs.ex: -------------------------------------------------------------------------------- 1 | defmodule Obanalyze.ObanJobs do 2 | import Ecto.Query, 3 | only: [from: 2, group_by: 3, order_by: 2, order_by: 3, select: 3, limit: 2, where: 3] 4 | 5 | @oban_name Application.compile_env(:obanalyze, :oban_name, Oban) 6 | 7 | defp oban_config(), do: Oban.config(@oban_name) 8 | 9 | def get_oban_job(job_id) do 10 | Oban.Repo.get(oban_config(), Oban.Job, job_id) 11 | end 12 | 13 | def fetch_oban_job(job_id) do 14 | case get_oban_job(job_id) do 15 | %Oban.Job{} = job -> {:ok, job} 16 | _ -> {:error, :not_found} 17 | end 18 | end 19 | 20 | def delete_oban_job(job_id) do 21 | query = from(oj in Oban.Job, where: oj.id in [^job_id]) 22 | Oban.Repo.delete_all(oban_config(), query) 23 | :ok 24 | end 25 | 26 | def retry_oban_job(job_id) do 27 | with {:ok, job} <- fetch_oban_job(job_id), 28 | :ok <- Oban.Engine.retry_job(oban_config(), job), 29 | {:ok, job} <- fetch_oban_job(job_id) do 30 | {:ok, job} 31 | end 32 | end 33 | 34 | def cancel_oban_job(job_id) do 35 | with {:ok, job} <- fetch_oban_job(job_id), 36 | :ok <- Oban.Engine.cancel_job(oban_config(), job), 37 | {:ok, job} <- fetch_oban_job(job_id) do 38 | {:ok, job} 39 | end 40 | end 41 | 42 | def list_jobs_with_total(params, job_state) do 43 | total_jobs = Oban.Repo.aggregate(oban_config(), jobs_count_query(job_state), :count) 44 | 45 | jobs = 46 | Oban.Repo.all(oban_config(), jobs_query(params, job_state)) |> Enum.map(&Map.from_struct/1) 47 | 48 | {jobs, total_jobs} 49 | end 50 | 51 | defp jobs_query(%{sort_by: sort_by, sort_dir: sort_dir, limit: limit} = params, job_state) do 52 | Oban.Job 53 | |> limit(^limit) 54 | |> where([job], job.state == ^job_state) 55 | |> order_by({^sort_dir, ^sort_by}) 56 | |> filter(params[:search]) 57 | end 58 | 59 | defp filter(query, nil), do: query 60 | 61 | defp filter(query, term) do 62 | like = "%#{term}%" 63 | 64 | if postgres?() do 65 | from oj in query, 66 | where: ilike(oj.worker, ^like), 67 | or_where: ilike(type(oj.args, :string), ^like) 68 | else 69 | from oj in query, 70 | where: like(oj.worker, ^like), 71 | or_where: like(type(oj.args, :string), ^like) 72 | end 73 | end 74 | 75 | defp postgres? do 76 | oban_config().engine in [Oban.Engines.Basic, Oban.Pro.Engines.Smart] 77 | end 78 | 79 | defp jobs_count_query(job_state) do 80 | Oban.Job 81 | |> where([job], job.state == ^job_state) 82 | end 83 | 84 | def job_states_with_count do 85 | Oban.Repo.all( 86 | oban_config(), 87 | Oban.Job 88 | |> group_by([j], [j.state]) 89 | |> order_by([j], [j.state]) 90 | |> select([j], {j.state, count(j.id)}) 91 | ) 92 | |> Enum.into(%{}) 93 | end 94 | 95 | def timestamp_field_for_job_state(job_state, default \\ :attempted_at) do 96 | case job_state do 97 | "available" -> :scheduled_at 98 | "cancelled" -> :cancelled_at 99 | "completed" -> :completed_at 100 | "discarded" -> :discarded_at 101 | "executing" -> :attempted_at 102 | "retryable" -> :scheduled_at 103 | "scheduled" -> :scheduled_at 104 | _ -> default 105 | end 106 | end 107 | 108 | def sorted_job_states do 109 | [ 110 | "executing", 111 | "available", 112 | "scheduled", 113 | "retryable", 114 | "cancelled", 115 | "discarded", 116 | "completed" 117 | ] 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Obanalyze.MixProject do 2 | use Mix.Project 3 | 4 | @version "1.4.2" 5 | @source_url "https://github.com/egze/obanalyze" 6 | 7 | def project do 8 | [ 9 | app: :obanalyze, 10 | version: @version, 11 | elixir: "~> 1.13", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | package: package(), 15 | name: "Obanalyze", 16 | docs: docs(), 17 | description: "Real-time Monitoring for Oban with Phoenix.LiveDashboard", 18 | source_url: @source_url 19 | ] 20 | end 21 | 22 | def application do 23 | [] 24 | end 25 | 26 | defp docs do 27 | [ 28 | main: "Obanalyze", 29 | source_ref: "v#{@version}", 30 | source_url: @source_url, 31 | nest_modules_by_prefix: [Obanalyze] 32 | ] 33 | end 34 | 35 | defp deps do 36 | [ 37 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 38 | {:phoenix_live_dashboard, ">= 0.8.5"}, 39 | {:floki, ">= 0.30.0", only: :test}, 40 | {:ecto_sqlite3, ">= 0.0.0", only: :test}, 41 | {:oban, "~> 2.15"} 42 | ] 43 | end 44 | 45 | defp package do 46 | [ 47 | maintainers: ["Aleksandr Lossenko"], 48 | licenses: ["MIT"], 49 | links: %{github: "https://github.com/egze/obanalyze"}, 50 | files: ~w(lib CHANGELOG.md LICENSE.md mix.exs README.md) 51 | ] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, 3 | "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, 4 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, 5 | "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 7 | "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, 8 | "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, 9 | "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.4", "48dd9c6d0fc10875a64545d04f0478b142898b6f0e73ae969becf5726f834d22", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "f67372e0eae5e5cbdd1d145e78e670fc5064d5810adf99d104d364cb920e306a"}, 10 | "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, 11 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 12 | "exqlite": {:hex, :exqlite, "0.27.0", "2ef6021862e74c6253d1fb1f5701bd47e4e779b035d34daf2a13ec83945a05ba", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "b947b9db15bb7aad11da6cd18a0d8b78f7fcce89508a27a5b9be18350fe12c59"}, 13 | "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, 14 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 15 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 16 | "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"}, 17 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 18 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 19 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 20 | "oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"}, 21 | "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, 22 | "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, 23 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.5", "d5f44d7dbd7cfacaa617b70c5a14b2b598d6f93b9caa8e350c51d56cd4350a9b", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1d73920515554d7d6c548aee0bf10a4780568b029d042eccb336db29ea0dad70"}, 24 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"}, 25 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 26 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 27 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, 28 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 29 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 30 | "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, 31 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 32 | "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, 33 | } 34 | -------------------------------------------------------------------------------- /test/obanalyze/dashboard_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Obanalyze.DashboardTest do 2 | use ExUnit.Case, async: false 3 | 4 | import Phoenix.ConnTest 5 | import Phoenix.LiveViewTest 6 | 7 | @endpoint Obanalyze.DashboardTest.Endpoint 8 | 9 | setup do 10 | Obanalyze.DashboardTest.Repo.delete_all(Oban.Job) 11 | :ok 12 | end 13 | 14 | test "menu_link/2" do 15 | assert {:ok, "Obanalyze"} = Obanalyze.Dashboard.menu_link(nil, nil) 16 | end 17 | 18 | test "shows jobs with limit" do 19 | for _ <- 1..110, do: job_fixture(%{}, state: "executing", attempted_at: DateTime.utc_now()) 20 | {:ok, live, rendered} = live(build_conn(), "/dashboard/obanalyze") 21 | assert_count(rendered, "executing", 20) 22 | 23 | rendered = render_patch(live, "/dashboard/obanalyze?limit=100") 24 | assert_count(rendered, "executing", 100) 25 | end 26 | 27 | test "shows job info modal" do 28 | job = 29 | job_fixture(%{something: "foobar"}, state: "executing", attempted_at: DateTime.utc_now()) 30 | 31 | {:ok, live, rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}") 32 | assert rendered =~ "modal-content" 33 | assert rendered =~ "foobar" 34 | 35 | refute live 36 | |> element("#modal-close") 37 | |> render_click() =~ "modal-close" 38 | end 39 | 40 | test "switch between states" do 41 | _executing_job = 42 | job_fixture(%{"foo" => "executing"}, state: "executing", attempted_at: DateTime.utc_now()) 43 | 44 | _completed_job = 45 | job_fixture(%{"foo" => "completed"}, state: "completed", completed_at: DateTime.utc_now()) 46 | 47 | conn = build_conn() 48 | {:ok, live, rendered} = live(conn, "/dashboard/obanalyze") 49 | 50 | assert_count(rendered, "executing", 1) 51 | 52 | {:ok, live, rendered} = 53 | live 54 | |> element("a", "Completed (1)") 55 | |> render_click() 56 | |> follow_redirect(conn) 57 | 58 | assert_count(rendered, "completed", 1) 59 | 60 | {:ok, _live, rendered} = 61 | live 62 | |> element("a", "Scheduled (0)") 63 | |> render_click() 64 | |> follow_redirect(conn) 65 | 66 | assert_count(rendered, "scheduled", 0) 67 | end 68 | 69 | test "run now job" do 70 | job = job_fixture(%{foo: "bar"}, schedule_in: 1000) 71 | {:ok, live, _rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}") 72 | 73 | assert has_element?(live, "pre", "scheduled") 74 | element(live, "button", "Run now") |> render_click() 75 | assert has_element?(live, "pre", "available") 76 | end 77 | 78 | test "retry job" do 79 | job = job_fixture(%{foo: "bar"}, state: "cancelled") 80 | {:ok, live, _rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}") 81 | 82 | assert has_element?(live, "pre", "cancelled") 83 | element(live, "button", "Retry") |> render_click() 84 | assert has_element?(live, "pre", "available") 85 | end 86 | 87 | test "cancel job" do 88 | job = job_fixture(%{foo: "bar"}, schedule_in: 1000) 89 | {:ok, live, _rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}") 90 | 91 | assert has_element?(live, "pre", "scheduled") 92 | element(live, "button", "Cancel") |> render_click() 93 | assert has_element?(live, "pre", "cancelled") 94 | end 95 | 96 | test "delete job" do 97 | job = job_fixture(%{foo: "bar"}, schedule_in: 1000) 98 | {:ok, live, _rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}") 99 | 100 | assert has_element?(live, "pre", "scheduled") 101 | element(live, "button", "Delete") |> render_click() 102 | assert_patched(live, "/dashboard/obanalyze?") 103 | end 104 | 105 | test "search" do 106 | _json_job = 107 | job_fixture(%{foo: "json"}, 108 | state: "executing", 109 | worker: "JsonWorker", 110 | attempted_at: DateTime.utc_now() 111 | ) 112 | 113 | _yaml_job = 114 | job_fixture(%{foo: "yaml"}, 115 | state: "executing", 116 | worker: "YamlWorker", 117 | attempted_at: DateTime.utc_now() 118 | ) 119 | 120 | {:ok, _live, rendered} = live(build_conn(), "/dashboard/obanalyze?search=JsonWorker") 121 | assert_count(rendered, 1) 122 | 123 | {:ok, _live, rendered} = live(build_conn(), "/dashboard/obanalyze?search=YamlWorker") 124 | assert_count(rendered, 1) 125 | 126 | {:ok, _live, rendered} = live(build_conn(), "/dashboard/obanalyze?search=yamlworker") 127 | assert_count(rendered, 1) 128 | 129 | {:ok, _live, rendered} = live(build_conn(), "/dashboard/obanalyze?search=foo") 130 | assert_count(rendered, 2) 131 | 132 | {:ok, _live, rendered} = live(build_conn(), "/dashboard/obanalyze?search=nothing") 133 | assert_count(rendered, 0) 134 | end 135 | 136 | defp assert_count(rendered, state \\ "executing", n) do 137 | assert length(:binary.matches(rendered, " Oban.insert() 143 | job 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /test/obanalyze_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ObanalyzeTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/support/migrations/20241110100000_add_oban_jobs_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Oban.Test.Repo.SQLite.Migrations.AddObanJobsTable do 2 | use Ecto.Migration 3 | 4 | defdelegate up, to: Oban.Migration 5 | defdelegate down, to: Oban.Migration 6 | end 7 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | System.put_env("PHX_DASHBOARD_TEST", "PHX_DASHBOARD_ENV_VALUE") 2 | 3 | Application.put_env(:obanalyze, Obanalyze.DashboardTest.Endpoint, 4 | url: [host: "localhost", port: 4000], 5 | secret_key_base: "QF9vsfJAwT1POSAZLc5sk4Rl4ZV5+fnxX7uos0cHZkT3P1tBQID3V5YsGQyDPKmT", 6 | live_view: [signing_salt: "QF9vsfJA"], 7 | render_errors: [view: Obanalyze.DashboardTest.ErrorView], 8 | check_origin: false, 9 | pubsub_server: Obanalyze.DashboardTest.PubSub 10 | ) 11 | 12 | Application.put_env(:obanalyze, Obanalyze.DashboardTest.Repo, 13 | database: System.get_env("SQLITE_DB") || "test.db", 14 | migration_lock: false 15 | ) 16 | 17 | defmodule Obanalyze.DashboardTest.Repo do 18 | use Ecto.Repo, otp_app: :obanalyze, adapter: Ecto.Adapters.SQLite3 19 | end 20 | 21 | _ = Ecto.Adapters.SQLite3.storage_up(Obanalyze.DashboardTest.Repo.config()) 22 | 23 | defmodule Obanalyze.DashboardTest.ErrorView do 24 | def render(template, _assigns) do 25 | Phoenix.Controller.status_message_from_template(template) 26 | end 27 | end 28 | 29 | defmodule Obanalyze.DashboardTest.Telemetry do 30 | import Telemetry.Metrics 31 | 32 | def metrics do 33 | [ 34 | counter("phx.b.c"), 35 | counter("phx.b.d"), 36 | counter("ecto.f.g"), 37 | counter("my_app.h.i") 38 | ] 39 | end 40 | end 41 | 42 | defmodule Obanalyze.DashboardTest.Router do 43 | use Phoenix.Router 44 | import Phoenix.LiveDashboard.Router 45 | 46 | pipeline :browser do 47 | plug(:fetch_session) 48 | end 49 | 50 | scope "/", ThisWontBeUsed, as: :this_wont_be_used do 51 | pipe_through(:browser) 52 | 53 | # Ecto repos will be auto discoverable. 54 | live_dashboard("/dashboard", 55 | metrics: Obanalyze.DashboardTest.Telemetry, 56 | additional_pages: [ 57 | obanalyze: Obanalyze.dashboard() 58 | ] 59 | ) 60 | end 61 | end 62 | 63 | defmodule Obanalyze.DashboardTest.Endpoint do 64 | use Phoenix.Endpoint, otp_app: :obanalyze 65 | 66 | plug(Phoenix.LiveDashboard.RequestLogger, 67 | param_key: "request_logger_param_key", 68 | cookie_key: "request_logger_cookie_key" 69 | ) 70 | 71 | plug(Plug.Session, 72 | store: :cookie, 73 | key: "_live_view_key", 74 | signing_salt: "QF9vsfJA" 75 | ) 76 | 77 | plug(Obanalyze.DashboardTest.Router) 78 | end 79 | 80 | Supervisor.start_link( 81 | [ 82 | {Phoenix.PubSub, name: Obanalyze.DashboardTest.PubSub, adapter: Phoenix.PubSub.PG2}, 83 | Obanalyze.DashboardTest.Repo, 84 | Obanalyze.DashboardTest.Endpoint, 85 | {Oban, testing: :manual, engine: Oban.Engines.Lite, repo: Obanalyze.DashboardTest.Repo}, 86 | {Ecto.Migrator, 87 | repos: [Obanalyze.DashboardTest.Repo], 88 | migrator: fn repo, :up, opts -> 89 | Ecto.Migrator.run(repo, Path.join([__DIR__, "support", "migrations"]), :up, opts) 90 | end} 91 | ], 92 | strategy: :one_for_one 93 | ) 94 | 95 | ExUnit.start() 96 | --------------------------------------------------------------------------------