From 2a75da2f2f90dc00764acfacf871c34ee7100a9b Mon Sep 17 00:00:00 2001 From: Marty Zalega Date: Mon, 7 Aug 2023 18:50:19 +1000 Subject: [PATCH] Big bang! --- .formatter.exs | 4 + .github/workflows/ci.yml | 51 +++++++ .gitignore | 4 + README.md | 37 ++++- lib/oban/live_dashboard.ex | 133 ++++++++++++++++++ mix.exs | 43 ++++++ mix.lock | 33 +++++ test/oban/live_dashboard_test.exs | 36 +++++ .../20221222130000_add_oban_jobs_table.exs | 6 + test/test_helper.exs | 95 +++++++++++++ 10 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 .formatter.exs create mode 100644 .github/workflows/ci.yml create mode 100644 lib/oban/live_dashboard.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/oban/live_dashboard_test.exs create mode 100644 test/support/migrations/20221222130000_add_oban_jobs_table.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..39a0592 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + ci: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - pair: + elixir-version: 1.14 + otp-version: 24.3 + lint: lint + - pair: + elixir-version: 1.15 + otp-version: 26.0 + lint: lint + steps: + - uses: actions/checkout@v3 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{matrix.pair.elixir-version}} + otp-version: ${{matrix.pair.otp-version}} + - name: Restore elixir dependencies cache + uses: actions/cache@v3 + with: + path: deps + key: ${{runner.os}}-mix-${{matrix.elixir-version}}-${{matrix.otp-version}}-${{hashFiles('**/mix.lock')}} + restore-keys: ${{runner.os}}-mix-${{matrix.elixir-version}}-${{matrix.otp-version}}- + - name: Install dependencies + run: mix deps.get --only test + - name: Check syntax formatting + run: mix format --check-formatted + if: ${{matrix.lint}} + - name: Check for unused dependencies + run: mix deps.unlock --check-unused + if: ${{matrix.lint}} + - name: Compile dependencies + run: mix deps.compile + - name: Compile + run: mix compile --warnings-as-errors + if: ${{matrix.lint}} + - name: Run tests + run: mix test diff --git a/.gitignore b/.gitignore index b263cd1..5a36b18 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ erl_crash.dump *.beam /config/*.secret.exs .elixir_ls/ +/.fetch +*.tar +/tmp/ +*.db* diff --git a/README.md b/README.md index 9660caa..710c2ac 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ -# oban_live_dashboard -A simple Phoenix Live Dashboard for Oban jobs. +# Oban Live Dashboard + +A simple [Phoenix Live Dashboard](https://github.com/phoenixframework/phoenix_live_dashboard) for [Oban](https://github.com/sorentwo/oban) jobs. + +## Installation + +Follow these steps to get going. + +### 1. Add the `oban_live_dashboard` dependency + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `oban_live_dashboard` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:oban_live_dashboard, "~> 0.1.0"} + ] +end +``` + +### 2. Update LiveView router + +Next you'll need to update the `live_dashboard` configuration in your router. + +```elixir +# lib/my_app_web/router.ex +live_dashboard "/dashboard", + additional_pages: [ + oban: Oban.LiveDashboard + ] +``` + +Then restart the server and access `/dashboard/oban`. + diff --git a/lib/oban/live_dashboard.ex b/lib/oban/live_dashboard.ex new file mode 100644 index 0000000..e4bffbb --- /dev/null +++ b/lib/oban/live_dashboard.ex @@ -0,0 +1,133 @@ +defmodule Oban.LiveDashboard do + use Phoenix.LiveDashboard.PageBuilder, refresher?: true + + import Phoenix.LiveDashboard.Helpers, only: [format_value: 2] + import Ecto.Query + + @impl true + def menu_link(_, _) do + {:ok, "Oban"} + end + + @impl true + def render(assigns) do + ~H""" + <.live_table + id="oban_jobs" + dom_id="oban-jobs" + page={@page} + row_attrs={&row_attrs/1} + row_fetcher={&fetch_jobs/2} + title="Oban Jobs" + search={false} + > + <:col field={:id} header="ID" sortable={:desc} /> + <:col field={:state} sortable={:desc} /> + <:col field={:queue} sortable={:desc} /> + <:col field={:worker} sortable={:desc} /> + <:col :let={job} field={:attempt} header="Attempts" sortable={:desc}> + <%= job.attempt %>/<%= job.max_attempts %> + + <:col :let={job} field={:inserted_at} sortable={:desc}> + <%= format_value(job.inserted_at) %> + + <:col :let={job} field={:scheduled_at} sortable={:desc}> + <%= format_value(job.scheduled_at) %> + + + <.live_modal + :if={@job != nil} + id="modal" + title="Job" + return_to={live_dashboard_path(@socket, @page, params: %{})} + > + <.label_value_list> + <:elem label="ID"><%= @job.id %> + <:elem label="State"><%= @job.state %> + <:elem label="Queue"><%= @job.queue %> + <:elem label="Worker"><%= @job.worker %> + <:elem label="Args"><%= format_value(@job.args, nil) %> + <:elem :if={@job.meta != %{}} label="Meta"><%= format_value(@job.meta, nil) %> + <:elem :if={@job.tags != []} label="Tags"><%= format_value(@job.tags, nil) %> + <:elem :if={@job.errors != []} label="Errors"><%= format_errors(@job.errors) %> + <:elem label="Attempts"><%= @job.attempt %>/<%= @job.max_attempts %> + <:elem label="Priority"><%= @job.priority %> + <:elem label="Attempted at"><%= format_value(@job.attempted_at) %> + <:elem :if={@job.cancelled_at} label="Cancelled at"> + <%= format_value(@job.cancelled_at) %> + + <:elem :if={@job.completed_at} label="Completed at"> + <%= format_value(@job.completed_at) %> + + <:elem :if={@job.discarded_at} label="Discarded at"> + <%= format_value(@job.discarded_at) %> + + <:elem label="Inserted at"><%= format_value(@job.inserted_at) %> + <:elem label="Scheduled at"><%= format_value(@job.scheduled_at) %> + + + """ + end + + @impl true + def handle_params(%{"params" => %{"job" => job_id}}, _url, socket) do + case fetch_job(job_id) do + {:ok, job} -> + {:noreply, assign(socket, job: job)} + + :error -> + to = live_dashboard_path(socket, socket.assigns.page, params: %{}) + {:noreply, push_patch(socket, to: to)} + end + end + + def handle_params(_params, _url, socket) do + {:noreply, assign(socket, job: nil)} + end + + @impl true + def handle_event("show_job", params, socket) do + to = live_dashboard_path(socket, socket.assigns.page, params: params) + {:noreply, push_patch(socket, to: to)} + end + + defp fetch_jobs(params, _node) do + total_jobs = Oban.Repo.aggregate(Oban.config(), Oban.Job, :count) + jobs = Oban.Repo.all(Oban.config(), jobs_query(params)) |> Enum.map(&Map.from_struct/1) + {jobs, total_jobs} + end + + defp fetch_job(id) do + case Oban.Repo.get(Oban.config(), Oban.Job, id) do + nil -> + :error + + job -> + {:ok, job} + end + end + + defp jobs_query(%{sort_by: sort_by, sort_dir: sort_dir, limit: l}) do + Oban.Job + |> limit(^l) + |> order_by({^sort_dir, ^sort_by}) + end + + defp row_attrs(job) do + [ + {"phx-click", "show_job"}, + {"phx-value-job", job[:id]}, + {"phx-page-loading", true} + ] + end + + defp format_errors(errors) do + Enum.map(errors, &Map.get(&1, "error")) + end + + def format_value(%DateTime{} = datetime) do + DateTime.to_string(datetime) + end + + def format_value(nil), do: nil +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..d91ef99 --- /dev/null +++ b/mix.exs @@ -0,0 +1,43 @@ +defmodule Oban.LiveDashboard.MixProject do + use Mix.Project + + @source_url "https://github.com/evilmarty/oban_live_dashboard" + + def project do + [ + app: :oban_live_dashboard, + version: "0.1.0", + elixir: "~> 1.12", + start_permanent: Mix.env() == :prod, + deps: deps(), + package: package(), + description: "A simple Phoenix Live Dashboard for Oban jobs", + source_url: @source_url + ] + end + + def application do + [] + end + + defp deps do + [ + {:ex_doc, ">= 0.0.0", only: [:dev, :test], runtime: false}, + {:phoenix_live_dashboard, "~> 0.7"}, + {:floki, ">= 0.30.0", only: :test}, + {:ecto_sqlite3, ">= 0.0.0", only: :test}, + {:oban, "~> 2.15"} + ] + end + + defp package do + [ + maintainers: ["Marty Zalega"], + licenses: ["Apache-2.0"], + files: ~w(lib .formatter.exs mix.exs README.md LICENSE), + links: %{ + GitHub: @source_url + } + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..9ef194a --- /dev/null +++ b/mix.lock @@ -0,0 +1,33 @@ +%{ + "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.8", "933a5f4da3b19ee56539a076076ce4d7716d64efc8db46fd066996a7e46e2bfd", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "176bdf4366956e456bf761b54ad70bc4103d0269ca9558fd7cee93d1b3f116db"}, + "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, + "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 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", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 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", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.10.3", "82ce316a8727f1daec397a9932b1a20130ea1ac33c3257b78eded1d3f45ae9b3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "b4fa32d09f5e5c05d3401ade3dd4416e3c7072d5117c150cb4adeea72760fb93"}, + "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, + "ex_doc": {:hex, :ex_doc, "0.30.4", "e8395c8e3c007321abb30a334f9f7c0858d80949af298302daf77553468c0c39", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9a19f0c50ffaa02435668f5242f2b2a61d46b541ebf326884505dfd3dd7af5e4"}, + "exqlite": {:hex, :exqlite, "0.13.14", "acd8b58c2245c6aa611262a887509c6aa862a05bfeb174faf348375bd9fc7edb", [: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.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "e81cd9b811e70a43b8d2d4ee76d3ce57ff349890ec4182f8f5223ead38ac4996"}, + "floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "oban": {:hex, :oban, "2.15.3", "d911af5362d707d0afd42bbead7eaa1d2708f55ece43a391e6ba246533b9d201", [:mix], [{:ecto_sql, "~> 3.6", [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", "2c57502710aa21165c358f0b6f3062e3b1b334f54d970747d91437369ef5cb4a"}, + "phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [: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.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [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", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [: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", [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.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{: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", [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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"}, + "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, + "websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [: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", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"}, +} diff --git a/test/oban/live_dashboard_test.exs b/test/oban/live_dashboard_test.exs new file mode 100644 index 0000000..e87f878 --- /dev/null +++ b/test/oban/live_dashboard_test.exs @@ -0,0 +1,36 @@ +defmodule Oban.LiveDashboardTest do + use ExUnit.Case, async: true + # use Oban.Testing, repo: Oban.LiveDashboardTest.Repo, prefix: false + + import Phoenix.ConnTest + import Phoenix.LiveViewTest + + @endpoint Oban.LiveDashboardTest.Endpoint + + test "menu_link/2" do + assert {:ok, "Oban"} = Oban.LiveDashboard.menu_link(nil, nil) + end + + test "shows jobs with limit" do + for _ <- 1..110, do: job_fixture() + {:ok, live, rendered} = live(build_conn(), "/dashboard/oban") + assert rendered |> :binary.matches(" length() <= 100 + + rendered = render_patch(live, "/dashboard/oban?limit=100") + assert rendered |> :binary.matches(" length() == 100 + end + + test "shows job info modal" do + job = job_fixture(%{something: "foobar"}) + {:ok, live, rendered} = live(build_conn(), "/dashboard/oban?params[job]=#{job.id}") + rendered = render(live) + assert rendered =~ "modal-content" + assert rendered =~ "%{"something" => "foobar"}" + refute live |> element("#modal-close") |> render_click() =~ "modal" + end + + defp job_fixture(args \\ %{}) do + {:ok, job} = Oban.Job.new(args, worker: "FakeWorker") |> Oban.insert() + job + end +end diff --git a/test/support/migrations/20221222130000_add_oban_jobs_table.exs b/test/support/migrations/20221222130000_add_oban_jobs_table.exs new file mode 100644 index 0000000..33de38e --- /dev/null +++ b/test/support/migrations/20221222130000_add_oban_jobs_table.exs @@ -0,0 +1,6 @@ +defmodule Oban.Test.Repo.SQLite.Migrations.AddObanJobsTable do + use Ecto.Migration + + defdelegate up, to: Oban.Migration + defdelegate down, to: Oban.Migration +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..608ebe7 --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,95 @@ +System.put_env("PHX_DASHBOARD_TEST", "PHX_DASHBOARD_ENV_VALUE") + +Application.put_env(:oban_live_dashboard, Oban.LiveDashboardTest.Endpoint, + url: [host: "localhost", port: 4000], + secret_key_base: "Hu4qQN3iKzTV4fJxhorPQlA/osH9fAMtbtjVS58PFgfw3ja5Z18Q/WSNR9wP4OfW", + live_view: [signing_salt: "hMegieSe"], + render_errors: [view: Oban.LiveDashboardTest.ErrorView], + check_origin: false, + pubsub_server: Oban.LiveDashboardTest.PubSub +) + +Application.put_env(:oban_live_dashboard, Oban.LiveDashboardTest.Repo, + database: System.get_env("SQLITE_DB") || "test.db", + migration_lock: false +) + +defmodule Oban.LiveDashboardTest.Repo do + use Ecto.Repo, otp_app: :oban_live_dashboard, adapter: Ecto.Adapters.SQLite3 +end + +_ = Ecto.Adapters.SQLite3.storage_up(Oban.LiveDashboardTest.Repo.config()) + +defmodule Oban.LiveDashboardTest.ErrorView do + def render(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end + +defmodule Oban.LiveDashboardTest.Telemetry do + import Telemetry.Metrics + + def metrics do + [ + counter("phx.b.c"), + counter("phx.b.d"), + counter("ecto.f.g"), + counter("my_app.h.i") + ] + end +end + +defmodule Oban.LiveDashboardTest.Router do + use Phoenix.Router + import Phoenix.LiveDashboard.Router + + pipeline :browser do + plug(:fetch_session) + end + + scope "/", ThisWontBeUsed, as: :this_wont_be_used do + pipe_through(:browser) + + # Ecto repos will be auto discoverable. + live_dashboard("/dashboard", + metrics: Oban.LiveDashboardTest.Telemetry, + additional_pages: [ + oban: Oban.LiveDashboard + ] + ) + end +end + +defmodule Oban.LiveDashboardTest.Endpoint do + use Phoenix.Endpoint, otp_app: :oban_live_dashboard + + plug(Phoenix.LiveDashboard.RequestLogger, + param_key: "request_logger_param_key", + cookie_key: "request_logger_cookie_key" + ) + + plug(Plug.Session, + store: :cookie, + key: "_live_view_key", + signing_salt: "/VEDsdfsffMnp5" + ) + + plug(Oban.LiveDashboardTest.Router) +end + +Supervisor.start_link( + [ + {Phoenix.PubSub, name: Oban.LiveDashboardTest.PubSub, adapter: Phoenix.PubSub.PG2}, + Oban.LiveDashboardTest.Repo, + Oban.LiveDashboardTest.Endpoint, + {Oban, testing: :manual, engine: Oban.Engines.Lite, repo: Oban.LiveDashboardTest.Repo}, + {Ecto.Migrator, + repos: [Oban.LiveDashboardTest.Repo], + migrator: fn repo, :up, opts -> + Ecto.Migrator.run(repo, Path.join([__DIR__, "support", "migrations"]), :up, opts) + end} + ], + strategy: :one_for_one +) + +ExUnit.start()