├── .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 | 
51 |
52 | ## Single job view
53 |
54 | 
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 |
--------------------------------------------------------------------------------
|