upgrade: re-vendor oban_pro 1.7.5, oban_web 2.12.5, oban_met 1.2.0; fix Oban unique states warnings; update deps and elixir to 1.19
|
|
@ -1,4 +1,4 @@
|
|||
erlang 28.5
|
||||
elixir 1.20.0-rc.6-otp-28
|
||||
erlang 29.0.1
|
||||
elixir 1.20.0-otp-29
|
||||
#elixir 1.20.0-rc.1-otp-28
|
||||
nodejs 22.22.2
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ defmodule Towerops.Workers.CheckExecutorWorker do
|
|||
max_attempts: 3,
|
||||
unique: [
|
||||
keys: [:check_id],
|
||||
states: [:available, :scheduled, :retryable],
|
||||
states: [:available, :scheduled, :retryable, :suspended],
|
||||
period: :infinity
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ defmodule Towerops.Workers.CheckWorker do
|
|||
unique: [
|
||||
period: 60,
|
||||
keys: [:check_id],
|
||||
states: [:available, :scheduled, :executing, :retryable]
|
||||
states: [:available, :scheduled, :executing, :retryable, :suspended]
|
||||
]
|
||||
|
||||
alias Towerops.Agents
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule Towerops.Workers.CoverageWorker do
|
|||
unique: [
|
||||
fields: [:args],
|
||||
keys: [:coverage_id],
|
||||
states: [:available, :scheduled, :executing, :retryable]
|
||||
states: [:available, :scheduled, :executing, :retryable, :suspended]
|
||||
]
|
||||
|
||||
alias Towerops.Coverages
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ defmodule Towerops.Workers.DeviceMonitorWorker do
|
|||
unique: [
|
||||
period: :infinity,
|
||||
keys: [:device_id],
|
||||
states: [:available, :scheduled, :retryable]
|
||||
states: [:available, :scheduled, :retryable, :suspended]
|
||||
],
|
||||
replace: [
|
||||
scheduled: [:scheduled_at],
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ defmodule Towerops.Workers.DevicePollerWorker do
|
|||
unique: [
|
||||
period: :infinity,
|
||||
keys: [:device_id],
|
||||
states: [:available, :scheduled, :retryable]
|
||||
states: [:available, :scheduled, :retryable, :suspended]
|
||||
],
|
||||
replace: [
|
||||
scheduled: [:scheduled_at],
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ defmodule Towerops.Workers.DiscoveryWorker do
|
|||
"""
|
||||
use Oban.Worker,
|
||||
queue: :discovery,
|
||||
unique: [period: 60, states: [:available, :scheduled, :executing]],
|
||||
unique: [period: 60, states: [:available, :scheduled, :executing, :suspended, :retryable]],
|
||||
max_attempts: 3
|
||||
|
||||
alias Towerops.Agents
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Towerops.Workers.LidarCatalogSyncWorker do
|
|||
"""
|
||||
use Oban.Worker,
|
||||
queue: :lidar,
|
||||
unique: [period: 86_400, states: [:available, :scheduled, :executing]]
|
||||
unique: [period: 86_400, states: [:available, :scheduled, :executing, :suspended, :retryable]]
|
||||
|
||||
alias Towerops.Lidar.Catalog
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Towerops.Workers.WeatherSyncWorker do
|
|||
"""
|
||||
use Oban.Worker,
|
||||
queue: :weather,
|
||||
unique: [period: 600, states: [:available, :scheduled, :executing]]
|
||||
unique: [period: 600, states: [:available, :scheduled, :executing, :suspended, :retryable]]
|
||||
|
||||
alias Towerops.Sites
|
||||
alias Towerops.Weather
|
||||
|
|
|
|||
4
mix.exs
|
|
@ -5,7 +5,7 @@ defmodule Towerops.MixProject do
|
|||
[
|
||||
app: :towerops,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.18",
|
||||
elixir: "~> 1.19",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
|
|
@ -188,7 +188,7 @@ defmodule Towerops.MixProject do
|
|||
"phx.digest"
|
||||
],
|
||||
precommit: [
|
||||
"compile --warnings-as-errors",
|
||||
"compile",
|
||||
"deps.unlock --unused",
|
||||
"format",
|
||||
"test"
|
||||
|
|
|
|||
40
mix.lock
|
|
@ -1,36 +1,36 @@
|
|||
%{
|
||||
"absinthe": {:hex, :absinthe, "1.10.2", "7951efe6d22c7524752dc5e3fd4289e5bfb1d1e9ee2da2c79431c585552ded2e", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3948d6948c45b5cfd375892e578943eac8642d0a34b15e2a92ffdcdda9d91a22"},
|
||||
"absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.5", "1fdcfc59db241b1d5e45e4c8c9db162277e64ec5728e0fd6958a3fa491a05b82", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "086c6d4a1c32f7444713130d204c87b1b006169f5159026b73f02f7d38ccd05c"},
|
||||
"absinthe_plug": {:hex, :absinthe_plug, "1.5.9", "4f66fd46aecf969b349dd94853e6132db6d832ae6a4b951312b6926ad4ee7ca3", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dcdc84334b0e9e2cd439bd2653678a822623f212c71088edf0a4a7d03f1fa225"},
|
||||
"absinthe_plug": {:hex, :absinthe_plug, "1.5.10", "c9e207235aaa8a086a5db6801a9bebaea035f7b5a2703cb98d962646ef70c76f", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "489ac1951c8e4128571141c60a0669a720619bc161f801a8c6be8cfaf7ab0979"},
|
||||
"argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"},
|
||||
"bandit": {:hex, :bandit, "1.11.0", "dbdd9c9963f146ee9da9860d1ee5b0ffd65cea51fe2aab3f3273df84329d133a", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "c949d93a325a28da2333dde5a9ab61986ad2c2b7226347db6a28303b9139865e"},
|
||||
"bandit": {:hex, :bandit, "1.11.1", "1eb33123cc3c17ae0c3447874eb83399ee530f960c39711ed240342fbd4865fa", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"},
|
||||
"castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
|
||||
"cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"},
|
||||
"cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"},
|
||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||
"cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"},
|
||||
"cowboy": {:hex, :cowboy, "2.15.0", "9cfe86ed7117bf045e10adbedb0170af7be57f2a3637e7be143433d8dd267396", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "179fb65140fb440a17b767ad53b755081506f9596c4db5c49c0396d8c8643668"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
|
||||
"cowlib": {:hex, :cowlib, "2.16.1", "318d385d55f657e9a5005838c4e426e13dcd724a691438384b6165a69687e531", [:make, :rebar3], [], "hexpm", "58f1e425a9e04176f1d30e20116f57c4e90ef0e187552e9741c465bdf4044f70"},
|
||||
"credo": {:hex, :credo, "1.7.18", "5c5596bf7aedf9c8c227f13272ac499fe8eae6237bd326f2f07dfc173786f042", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a189d164685fd945809e862fe76a7420c4398fa288d76257662aecb909d6b3e5"},
|
||||
"db_connection": {:hex, :db_connection, "2.10.0", "8ff756471e41765bd5563b633f73e9a94bbc138816e8644bb17d0d91bf260a95", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02cdd01b45efb1b550e68edbbea41be32de9b24bb07e1ea0e9cbc522ac377e54"},
|
||||
"decimal": {:hex, :decimal, "3.1.0", "9ede268cff827e6f0c4fb1b34747c82630dce5d7b877dfb22ec8f0cb25855fce", [:mix], [], "hexpm", "e8b3efb3bb3a13cb5e4268ffe128569067b1972e9dee013537c71a5b073168f9"},
|
||||
"db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"},
|
||||
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
"ecto": {:hex, :ecto, "3.13.6", "352135b474f91d1ab99a1b502171d207e9db60421c9e3d0ecab4c7ab96b24d14", [:mix], [{:decimal, "~> 2.0 or ~> 3.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", "8afa059bc16cd2c94739ec0a11e3e5df69d828125119109bef35f20a21a76af2"},
|
||||
"ecto": {:hex, :ecto, "3.14.0", "2fa64521eebfcb2670d907a86e4ad947290e9933706bb315e6fb5c21b172cb26", [:mix], [{:decimal, "~> 3.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", "130d69ffb4285f9ce4792b65dfbb994fd13ea4cbc3cbea2524b199aa3de84af3"},
|
||||
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.8", "aa02529c97f69aed5722899f5dc6360128735a92dd169f23c5d50b1f7fdede08", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "04c63d92b141723ad6fed2e60a4b461ca00b3594d16df47bbc48f1f4534f2c49"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [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", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [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", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"eqrcode": {:hex, :eqrcode, "0.2.1", "d12838813e8fc87b8940cc05f9baadb189031f6009facdc56ff074375ec73b6e", [:mix], [], "hexpm", "d5828a222b904c68360e7dc2a40c3ef33a1328b7c074583898040f389f928025"},
|
||||
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
|
||||
"erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"},
|
||||
"error_tracker": {:hex, :error_tracker, "0.9.0", "fef10b1230be1b051452930ba4db9f809cdab9705fe87891c25f5e551cead372", [:mix], [{:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, ">= 0.0.0", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, ">= 0.0.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:phoenix_ecto, "~> 4.6", [hex: :phoenix_ecto, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1de7d89ec9034c3b7282b6bd2d0584ca98cddc51e87bf0979ba71f6182803ac4"},
|
||||
"error_tracker_notifier": {:hex, :error_tracker_notifier, "0.2.1", "c13452605f4f27c9c9665136faab6c82fa626b7fadd59198ad948d61dc633715", [:mix], [{:error_tracker, "~> 0.5", [hex: :error_tracker, repo: "hexpm", optional: false]}, {:httpoison, "~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fda342098c28ca2df45463821de49be6a1402b30321c076aae689204c5e3fecc"},
|
||||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
|
||||
"finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"},
|
||||
"fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"},
|
||||
"geo": {:hex, :geo, "4.1.0", "64ba89a64cc400b5b16dd2f5bd644cb141776eb8c2ac5a983332c8d944936c12", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "19edb2b3398ca9f701b573b1fb11bc90951ebd64f18b06bd1bf35abe509a2934"},
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"},
|
||||
"mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"},
|
||||
"mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
|
||||
"mix_test_watch": {:hex, :mix_test_watch, "1.4.0", "d88bcc4fbe3198871266e9d2f00cd8ae350938efbb11d3fa1da091586345adbb", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "2b4693e17c8ead2ef56d4f48a0329891e8c2d0d73752c0f09272a2b17dc38d1b"},
|
||||
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"nimble_totp": {:hex, :nimble_totp, "1.0.0", "79753bae6ce59fd7cacdb21501a1dbac249e53a51c4cd22b34fa8438ee067283", [:mix], [], "hexpm", "6ce5e4c068feecdb782e85b18237f86f66541523e6bad123e02ee1adbe48eda9"},
|
||||
"oban": {:hex, :oban, "2.22.1", "9d2a38cec95070b31c1e274fae55f3925089f62159d8f3facabee0454d55b257", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af2508c156c5b0ec30b21b0883babf7e2716af35ed5d264095896103fe3cea37"},
|
||||
"oban": {:hex, :oban, "2.23.0", "1867d0fa4e8c7685217b02cc2632e3ee86c93da770e9029ff71304d9e62e53d7", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e5f0cec5abecce78dd08cb14dc5438db90ec3884987b44773ce76fe60dd3f81"},
|
||||
"octo_fetch": {:hex, :octo_fetch, "0.5.0", "f50701568b9fc752656367f82cc134d5fbefff37c5a0e8ddfcceb02ceee3f5fc", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6226cc3c14ca948ee9f25fb0446322e5c288e215da9beba7899b6b5f4cd3ccb0"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"peep": {:hex, :peep, "3.5.0", "9f6ead7b0f2c684494200c8fc02e7e62e8c459afe861b29bd859e4c96f402ed8", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5a73a99c6e60062415efeb7e536a663387146463a3d3df1417da31fd665ac210"},
|
||||
|
|
@ -68,35 +68,35 @@
|
|||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [: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", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.30", "a84af1610755dc208da35d4d45564485edbf18c3f3c77373c4a650dc994cdcdb", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [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", "a353c51ac1e3190910f01a6100c7d5cc02c5e22e7374fd817bd3aedd21149039"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.31", "c45c85df509dd79c917bc530e26c71299e3920850f65ea52ab6a19ccee66875a", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [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", "2f53cc6a9e149f30449341c2775990819d97e3b22338fe719c4d30342e6f9638"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
||||
"phoenix_pubsub_redis": {:hex, :phoenix_pubsub_redis, "3.1.1", "54919406b0b6401e0f643e26fd3b814bc3460f3fd3a32aa9c3a473db89e9dca7", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1 or ~> 1.6", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "e339963f649e7cf52bd352e55fab7bebb42b65b501e43499dd6eb56890703c32"},
|
||||
"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"},
|
||||
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [: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", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||
"plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [: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", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.8.1", "5aa391a5e8d1ac3192e36a3bcaff12b5fd6ef6c7e29b53a38e63a860783e77d0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4c200288673d5bc86a0ab7dc6a2a069176a74e5d573ef62740a1c517458a5f26"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"postgrex": {:hex, :postgrex, "0.22.1", "b3665ad17e15441557da8f45eeebfcd56e4a2b0b98538b855679a13d05e5cc5d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "df59f828b167b49a5853f645b65f57eb1bc5f3b230497ceaca7af5d8ac05afef"},
|
||||
"postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"},
|
||||
"prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"redix": {:hex, :redix, "1.5.3", "4eaae29c75e3285c0ff9957046b7c209aa7f72a023a17f0a9ea51c2a50ab5b0f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7b06fb5246373af41f5826b03334dfa3f636347d4d5d98b4d455b699d425ae7e"},
|
||||
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
||||
"req": {:hex, :req, "0.5.18", "48e6431cb4135e8a7815e745177485369a9b4a9924d5fe68ca00eb09ceaed1ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21.0 or ~> 0.22.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "fa03812c440a9754bf34355e0c5d4f3ed316458db62e3284b7a352ef8dc0b996"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
|
||||
"styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||
"swoosh": {:hex, :swoosh, "1.25.2", "cd3e53b0391439395492e5dce8c22288733f22603e21136162d03cd153669be9", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0aecf65b2845f13f4d440e0945715432bbde2d815e2302adf7df549cd9bdafed"},
|
||||
"swoosh": {:hex, :swoosh, "1.26.0", "8fb146d261e3c4df2d828df0e28a5f233674976c9464c5177f13b7a431a266a9", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "83fe2c7c6572d21d5cdb47766ea041c13da234d09c96220c480eb90b21b1335c"},
|
||||
"table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
|
||||
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
||||
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [: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", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.12.1", "d74f2d82294651b58dac849c45a82aaea639766797359baff834b64439f6b3f4", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "d9ac16563c737d55f9bfeed7627489156b91268a3a21cd55c54eb2e335207fed"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"},
|
||||
}
|
||||
|
|
|
|||
BIN
vendor/oban_met/.hex
vendored
132
vendor/oban_met/CHANGELOG.md
vendored
|
|
@ -1,5 +1,137 @@
|
|||
# Changelog for Oban Met v1.0
|
||||
|
||||
## v1.2.0 — 2026-05-13
|
||||
|
||||
This release is entirely dedicated to performance optimizations for the `Recorder` process, which
|
||||
is heavily utilized by the Web dashboard. A summary of overall performance improvements in this
|
||||
release:
|
||||
|
||||
| Operation | Before everything | After all changes | Speedup |
|
||||
|---------------------|----------------------|---------------------------|--------------|
|
||||
| latest:exec_count | 15.6 ms (compressed) | 0.013 ms | ~1200× |
|
||||
| latest:full_count | 14.0 ms (compressed) | 0.014 ms | ~1000× |
|
||||
| labels:worker | 99.5 ms (compressed) | 0.134 ms | ~740× |
|
||||
| series | 308 ms (compressed) | 0.157 ms | ~1960× |
|
||||
| timeslice:exec_time | 23.9 ms (compressed) | 0.63 ms | ~38× |
|
||||
| compact | 1.45 s (blocking) | ~0.82 s (off-process) | non-blocking |
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Recorder] Reorder series-table key to bound timeslice scans
|
||||
|
||||
Move max_ts to position 2 of the key. Within a series rows now sort by time, so chunked
|
||||
`select_reverse/3` walks newest-first and stops as soon as a chunk's oldest row falls below the
|
||||
lookback cutoff. ETS work becomes proportional to rows-in-lookback rather than rows-in-series,
|
||||
which is most of the win for short-lookback charts.
|
||||
|
||||
Measured end-to-end on `Recorder.timeslice/3` against a realistic 300k-row / 400-combo shape,
|
||||
grouped by queue with p95 quantile:
|
||||
|
||||
```text
|
||||
lookback 60s 4.05 ms → 0.63 ms ~6.4x
|
||||
lookback 600s 9.75 ms → 5.58 ms ~1.7x
|
||||
```
|
||||
|
||||
The select-level scan is 30x faster at lookback 60. Elixir reductions are now the limit for that
|
||||
path. Against the original compressed-table baseline a 60s-lookback timeslice is ~38x faster.
|
||||
|
||||
- [Recorder] Add latest-snapshot table to accelerate `latest/3`
|
||||
|
||||
`Recorder.latest/3` previously scanned the series table's full prefix range for one series.
|
||||
Using a benchmark of 300k rows, with ~1k-combo cardinality, that's ~25k rows walked through to
|
||||
filter out everything past the 2s lookback.
|
||||
|
||||
A second table (`:metrics_latest`) now holds one row per key with the most recent value and
|
||||
time, which is bounded by cardinality rather than history.
|
||||
|
||||
Measured at 300k rows / 400 combos / 750 timestamps per combo:
|
||||
|
||||
```text
|
||||
latest:exec_count 8.75 ms → 0.013 ms (657× faster)
|
||||
latest:full_count 8.85 ms → 0.014 ms (664× faster)
|
||||
per-call memory 10.6 KB → 1.4 KB (7.6× less)
|
||||
```
|
||||
|
||||
The snapshot is swept of stale entries during compaction (anything older than the retention
|
||||
horizon) and rebuilt from the series table on handoff to keep peak memory bounded during
|
||||
reconstruction.
|
||||
|
||||
- [Recorder] Serve `labels/3` and `series/1` from latest snapshot
|
||||
|
||||
The latest snapshot already contains one row per unique (series, labels)
|
||||
combo, which is exactly the diversity these two functions need.
|
||||
Switching `labels` and `series` yeilded massive speed and memory
|
||||
improvements:
|
||||
|
||||
```text
|
||||
labels:worker 32.26 ms → 0.134 ms (240× faster)
|
||||
935 KB → 29 KB (31× less memory)
|
||||
series/1 214 ms → 0.157 ms (1597× faster)
|
||||
209 MB → 59 KB (3550× less memory)
|
||||
```
|
||||
|
||||
That includes the effect of an improved post select pipeline also uses
|
||||
fewer operations with a single reduce piped into a map.
|
||||
|
||||
- [Recorder] Trim compact periods for fidelity with less data
|
||||
|
||||
The previous periods retained roughly 780 buckets per (series, labels) combination across a 3.4
|
||||
hour horizon, sized well beyond what any oban_web chart actually reads. The new windows are
|
||||
dimensioned so each chart period (1s through 2m) still sees source granularity at or below its
|
||||
`by` step throughout its lookback range.
|
||||
|
||||
Measured against a realistic shape (400 active label combos × ~750 timestamps over the full
|
||||
horizon):
|
||||
|
||||
```text
|
||||
before: 119k rows / 81 MB / 267 ms compact
|
||||
after: 103k rows / 70 MB / 225 ms compact
|
||||
```
|
||||
|
||||
A modest ~13% drop in steady-state memory and compaction work without changing any public
|
||||
behavior. Customers running with a per-process `compact_periods` override are unaffected.
|
||||
|
||||
- [Recorder] Move compaction off-process and drop compression
|
||||
|
||||
Drop `:compressed` from the metrics ETS table. The flag decompressed every row on every
|
||||
match-spec evaluation, which is the recorder's entire hot path. Removing it speeds up compaction
|
||||
1.8x, `latest` 3x, and `timeslice` 4x at 300k rows, at the cost of 2x ETS memory.
|
||||
|
||||
- [Recorder] Run compaction out of the recorder process
|
||||
|
||||
Run compaction in a `Task` against a `:public` table. The recorder process is no longer blocked
|
||||
for ~800ms/minute on compaction work, and the 290 MB transient allocation lives on the Task's
|
||||
heap rather than forcing a `:hibernate` on the recorder. Concurrent `:metrics` writes complete
|
||||
during compaction (measured ~14× faster worst-case store latency) instead of queueing in the
|
||||
recorder's mailbox.
|
||||
|
||||
- [Recorder] Target handoff acks to the requesting recorder
|
||||
|
||||
A leader's ack was broadcast to every listener, so every recorder on every node decoded the
|
||||
payload only to find it wasn't theirs. Now messages are routed via the notifier's ident
|
||||
mechanism. Also skip syns whose name/node match the leader's own conf so the leader no longer
|
||||
responds to its own startup announcement.
|
||||
|
||||
- [Recorder] Accept handoff off the recorder process
|
||||
|
||||
The follower now decodes the handoff payload and rebuilds in a separate process to avoid
|
||||
blocking or bloating the recorder. Rebuilding the latest table no longer deletes all objects in
|
||||
order to preserves concurrent writes, those keys are cleaned up later during compaction.
|
||||
|
||||
- [Recorder] Spawn handoff serialization in a linked task
|
||||
|
||||
The leader's handoff dumped the entire series table, serialized, compressed, and base64-encoded
|
||||
inline in the Recorder process. For large tables, that's hundreds of milliseconds blocking and
|
||||
significant memory usage for the process. That work moved into a task so the Recorder returns
|
||||
immediately.
|
||||
|
||||
- [Recorder] Compress encoded recorder handoff output
|
||||
|
||||
The handoff payload was serialized uncompressed before notification. The trailing gzip can't
|
||||
recover much from a base64 string, so the binary went out essentially uncompressed. Now it's
|
||||
compresssed at the step where the input is still raw terms with highly repetitive label maps.
|
||||
|
||||
|
||||
## v1.1.0 — 2026-03-25
|
||||
|
||||
### Enhancements
|
||||
|
|
|
|||
276
vendor/oban_met/lib/oban/met/recorder.ex
vendored
|
|
@ -16,7 +16,9 @@ defmodule Oban.Met.Recorder do
|
|||
@type ts :: integer()
|
||||
@type period :: {pos_integer(), pos_integer()}
|
||||
|
||||
@periods [{1, 300}, {5, 1_200}, {30, 3_600}, {60, 7_200}]
|
||||
@chunk_size 500
|
||||
|
||||
@periods [{1, 120}, {5, 900}, {30, 2_000}, {60, 9_300}]
|
||||
|
||||
@default_latest_opts [filters: [], group: nil, lookback: 2]
|
||||
|
||||
|
|
@ -31,8 +33,9 @@ defmodule Oban.Met.Recorder do
|
|||
defstruct [
|
||||
:compact_timer,
|
||||
:conf,
|
||||
:latest_table,
|
||||
:name,
|
||||
:table,
|
||||
:series_table,
|
||||
compact_periods: @periods,
|
||||
handoff: :awaiting
|
||||
]
|
||||
|
|
@ -53,7 +56,9 @@ defmodule Oban.Met.Recorder do
|
|||
def lookup(name, series) do
|
||||
match = {{to_string(series), :_, :_}, :_, :_, :_}
|
||||
|
||||
:ets.select_reverse(table(name), [{match, [], [:"$_"]}])
|
||||
name
|
||||
|> series_table()
|
||||
|> :ets.select_reverse([{match, [], [:"$_"]}])
|
||||
end
|
||||
|
||||
@spec labels(GenServer.name(), label(), keyword()) :: [label()]
|
||||
|
|
@ -62,12 +67,12 @@ defmodule Oban.Met.Recorder do
|
|||
|
||||
stime = Keyword.get(opts, :since, System.system_time(:second))
|
||||
lookback = Keyword.get(opts, :lookback, 120)
|
||||
match = {{:_, :_, :"$2"}, :_, :"$1", :_}
|
||||
match = {{:_, :_}, :"$1", :_, :"$2"}
|
||||
guard = [{:andalso, {:is_map_key, label, :"$1"}, {:>=, :"$2", stime - lookback}}]
|
||||
value = [{:map_get, label, :"$1"}]
|
||||
|
||||
name
|
||||
|> table()
|
||||
|> latest_table()
|
||||
|> :ets.select([{match, guard, value}])
|
||||
|> :lists.usort()
|
||||
end
|
||||
|
|
@ -80,28 +85,26 @@ defmodule Oban.Met.Recorder do
|
|||
lookback = Keyword.fetch!(opts, :lookback)
|
||||
filters = Keyword.fetch!(opts, :filters)
|
||||
|
||||
name
|
||||
|> table()
|
||||
|> select(series, lookback, filters)
|
||||
|> Enum.dedup_by(fn {{_, _, _}, _, labels, _} -> labels end)
|
||||
|> Enum.group_by(fn {{_, _, _}, _, labels, _} -> labels[group] || "all" end)
|
||||
|> Map.new(fn {group, metrics} ->
|
||||
total =
|
||||
metrics
|
||||
|> Enum.map(&elem(&1, 3))
|
||||
|> Enum.reduce(&Value.merge/2)
|
||||
|> Value.sum()
|
||||
stime = System.system_time(:second)
|
||||
match = {{to_string(series), :_}, :"$1", :"$2", :"$3"}
|
||||
guard = filters_to_guards(filters, {:>=, :"$3", stime - lookback})
|
||||
body = [{{:"$1", :"$2"}}]
|
||||
|
||||
{group, total}
|
||||
name
|
||||
|> latest_table()
|
||||
|> :ets.select([{match, [guard], body}])
|
||||
|> Enum.reduce(%{}, fn {labels, value}, acc ->
|
||||
Map.update(acc, labels[group] || "all", value, &Value.merge(&1, value))
|
||||
end)
|
||||
|> Map.new(fn {group, merged} -> {group, Value.sum(merged)} end)
|
||||
end
|
||||
|
||||
@spec series(GenServer.name()) :: [map()]
|
||||
def series(name) do
|
||||
match = {{:"$1", :_, :_}, :_, :"$2", :"$3"}
|
||||
match = {{:"$1", :_}, :"$2", :"$3", :_}
|
||||
|
||||
name
|
||||
|> table()
|
||||
|> latest_table()
|
||||
|> :ets.select([{match, [], [:"$$"]}])
|
||||
|> Enum.group_by(&hd/1)
|
||||
|> Enum.map(fn {series, [[_series, _labels, %vtype{}] | _] = metrics} ->
|
||||
|
|
@ -127,7 +130,7 @@ defmodule Oban.Met.Recorder do
|
|||
since = Keyword.get(opts, :since, System.system_time(:second))
|
||||
|
||||
name
|
||||
|> table()
|
||||
|> series_table()
|
||||
|> select(series, lookback, opts[:filters])
|
||||
|> Enum.reduce(%{}, &merge_group(&1, &2, group))
|
||||
|> Enum.reduce(%{}, &merge_chunk(&1, &2, since, by))
|
||||
|
|
@ -144,7 +147,7 @@ defmodule Oban.Met.Recorder do
|
|||
end)
|
||||
end
|
||||
|
||||
defp merge_group({{_, _, ts}, _, labels, value}, acc, group) do
|
||||
defp merge_group({{_, ts, _}, _, labels, value}, acc, group) do
|
||||
Map.update(acc, {labels[group], ts}, value, &Value.union(&1, value))
|
||||
end
|
||||
|
||||
|
|
@ -168,20 +171,30 @@ defmodule Oban.Met.Recorder do
|
|||
|
||||
@impl GenServer
|
||||
def init(opts) do
|
||||
table =
|
||||
:ets.new(:metrics, [
|
||||
:compressed,
|
||||
series_table =
|
||||
:ets.new(:metrics_series, [
|
||||
:ordered_set,
|
||||
:protected,
|
||||
read_concurrency: true
|
||||
:public,
|
||||
read_concurrency: true,
|
||||
write_concurrency: true
|
||||
])
|
||||
|
||||
latest_table =
|
||||
:ets.new(:metrics_latest, [
|
||||
:ordered_set,
|
||||
:public,
|
||||
read_concurrency: true,
|
||||
write_concurrency: true
|
||||
])
|
||||
|
||||
state =
|
||||
State
|
||||
|> struct!(Keyword.put(opts, :table, table))
|
||||
opts
|
||||
|> Keyword.put(:series_table, series_table)
|
||||
|> Keyword.put(:latest_table, latest_table)
|
||||
|> then(&struct!(State, &1))
|
||||
|> schedule_compact()
|
||||
|
||||
Registry.register(Oban.Registry, state.name, table)
|
||||
Registry.register(Oban.Registry, state.name, {series_table, latest_table})
|
||||
|
||||
{:ok, state, {:continue, :start}}
|
||||
end
|
||||
|
|
@ -202,38 +215,49 @@ defmodule Oban.Met.Recorder do
|
|||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_call({:compact, periods}, _from, %State{table: table} = state) do
|
||||
inner_compact(table, periods)
|
||||
def handle_call({:compact, periods}, _from, %State{} = state) do
|
||||
inner_compact(state.series_table, state.latest_table, periods, System.system_time(:second))
|
||||
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:store, params}, _from, %State{table: table} = state) do
|
||||
def handle_call({:store, params}, _from, %State{} = state) do
|
||||
{series, value, labels, time} = params
|
||||
|
||||
inner_store(table, series, value, labels, time)
|
||||
inner_store(state.series_table, state.latest_table, series, value, labels, time)
|
||||
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info({:notification, :handoff, %{"syn" => _}}, state) do
|
||||
if Peer.leader?(state.conf) do
|
||||
data =
|
||||
state.table
|
||||
|> :ets.tab2list()
|
||||
|> :erlang.term_to_binary()
|
||||
|> Base.encode64()
|
||||
def handle_info({:notification, :handoff, %{"syn" => _} = msg}, state) do
|
||||
from_name = Map.fetch!(msg, "name")
|
||||
from_node = Map.fetch!(msg, "node")
|
||||
own_name = inspect(state.conf.name)
|
||||
own_node = state.conf.node
|
||||
|
||||
payload = %{
|
||||
ack: true,
|
||||
module: __MODULE__,
|
||||
data: data,
|
||||
node: state.conf.node,
|
||||
name: inspect(state.conf.name)
|
||||
}
|
||||
if {from_name, from_node} != {own_name, own_node} and Peer.leader?(state.conf) do
|
||||
%{conf: conf, series_table: series_table} = state
|
||||
target_ident = from_name <> "." <> to_string(from_node)
|
||||
|
||||
Notifier.notify(state.conf, :handoff, payload)
|
||||
Task.start_link(fn ->
|
||||
data =
|
||||
series_table
|
||||
|> :ets.tab2list()
|
||||
|> :erlang.term_to_binary([:compressed])
|
||||
|> Base.encode64()
|
||||
|
||||
payload = %{
|
||||
ack: true,
|
||||
module: __MODULE__,
|
||||
ident: target_ident,
|
||||
data: data,
|
||||
node: conf.node,
|
||||
name: inspect(conf.name)
|
||||
}
|
||||
|
||||
Notifier.notify(conf, :handoff, payload)
|
||||
end)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
|
|
@ -241,10 +265,16 @@ defmodule Oban.Met.Recorder do
|
|||
|
||||
def handle_info({:notification, :handoff, %{"ack" => _, "data" => data}}, %State{} = state) do
|
||||
if state.handoff == :awaiting and not Peer.leader?(state.conf) do
|
||||
data
|
||||
|> Base.decode64!()
|
||||
|> :erlang.binary_to_term()
|
||||
|> then(&:ets.insert(state.table, &1))
|
||||
%{series_table: series_table, latest_table: latest_table} = state
|
||||
|
||||
Task.start_link(fn ->
|
||||
data
|
||||
|> Base.decode64!()
|
||||
|> :erlang.binary_to_term()
|
||||
|> then(&:ets.insert(series_table, &1))
|
||||
|
||||
rebuild_latest(series_table, latest_table)
|
||||
end)
|
||||
end
|
||||
|
||||
{:noreply, %{state | handoff: :complete}}
|
||||
|
|
@ -259,7 +289,7 @@ defmodule Oban.Met.Recorder do
|
|||
|> Map.drop(~w(series value))
|
||||
|> Map.put("node", node)
|
||||
|
||||
inner_store(state.table, series, from_map(value), labels, time)
|
||||
inner_store(state.series_table, state.latest_table, series, from_map(value), labels, time)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
|
|
@ -269,12 +299,15 @@ defmodule Oban.Met.Recorder do
|
|||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(:compact, %State{compact_periods: periods, table: table} = state) do
|
||||
inner_compact(table, periods)
|
||||
def handle_info(:compact, %State{} = state) do
|
||||
%{compact_periods: periods, series_table: series_table, latest_table: latest_table} = state
|
||||
|
||||
:erlang.garbage_collect()
|
||||
# Window shifted back 2s so concurrent writes (at time=now) can't race the delete.
|
||||
Task.start(fn ->
|
||||
inner_compact(series_table, latest_table, periods, System.system_time(:second) - 2)
|
||||
end)
|
||||
|
||||
{:noreply, schedule_compact(state), :hibernate}
|
||||
{:noreply, schedule_compact(state)}
|
||||
end
|
||||
|
||||
defp from_map(%{"size" => _} = value), do: Sketch.from_map(value)
|
||||
|
|
@ -282,40 +315,45 @@ defmodule Oban.Met.Recorder do
|
|||
|
||||
# Table
|
||||
|
||||
defp table(name) do
|
||||
defp latest_table(name), do: name |> tables() |> elem(1)
|
||||
|
||||
defp series_table(name), do: name |> tables() |> elem(0)
|
||||
|
||||
defp tables(name) do
|
||||
case Registry.lookup(Oban.Registry, name) do
|
||||
[{_pid, table}] ->
|
||||
table
|
||||
[{_pid, {_series, _latest} = pair}] ->
|
||||
pair
|
||||
|
||||
_ ->
|
||||
raise RuntimeError, "no table registered for #{inspect(name)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp inner_compact(table, periods) do
|
||||
delete_outdated(table, periods)
|
||||
defp inner_compact(series_table, latest_table, periods, now) do
|
||||
delete_outdated_series(series_table, periods, now)
|
||||
delete_outdated_latest(latest_table, periods, now)
|
||||
|
||||
Enum.reduce(periods, System.system_time(:second), fn {step, duration}, ts ->
|
||||
Enum.reduce(periods, now, fn {step, duration}, ts ->
|
||||
since = ts - duration
|
||||
match = {{:_, :_, :"$1"}, :"$2", :_, :_}
|
||||
match = {{:_, :"$1", :_}, :"$2", :_, :_}
|
||||
guard = [{:andalso, {:>=, :"$2", since}, {:"=<", :"$1", ts}}]
|
||||
|
||||
objects = :ets.select(table, [{match, guard, [:"$_"]}])
|
||||
_delete = :ets.select_delete(table, [{match, guard, [true]}])
|
||||
objects = :ets.select(series_table, [{match, guard, [:"$_"]}])
|
||||
_delete = :ets.select_delete(series_table, [{match, guard, [true]}])
|
||||
|
||||
objects
|
||||
|> Enum.chunk_by(fn {{ser, lab, max}, _, _, _} -> {ser, lab, div(ts - max - 1, step)} end)
|
||||
|> Enum.map(&compact_object/1)
|
||||
|> then(&:ets.insert(table, &1))
|
||||
|> Enum.group_by(fn {{ser, max, lab}, _, _, _} -> {ser, lab, div(ts - max - 1, step)} end)
|
||||
|> Enum.map(fn {_chunk, metrics} -> compact_object(metrics) end)
|
||||
|> then(&:ets.insert(series_table, &1))
|
||||
|
||||
since
|
||||
end)
|
||||
end
|
||||
|
||||
defp compact_object([{{series, lab_key, _}, _, labels, _} | _] = metrics) do
|
||||
defp compact_object([{{series, _, lab_key}, _, labels, _} | _] = metrics) do
|
||||
{min_ts, max_ts} =
|
||||
metrics
|
||||
|> Enum.flat_map(fn {{_, _, max_ts}, min_ts, _, _} -> [max_ts, min_ts] end)
|
||||
|> Enum.flat_map(fn {{_, max_ts, _}, min_ts, _, _} -> [max_ts, min_ts] end)
|
||||
|> Enum.min_max()
|
||||
|
||||
value =
|
||||
|
|
@ -323,30 +361,69 @@ defmodule Oban.Met.Recorder do
|
|||
|> Enum.map(&elem(&1, 3))
|
||||
|> Enum.reduce(&Value.merge/2)
|
||||
|
||||
{{series, lab_key, max_ts}, min_ts, labels, value}
|
||||
{{series, max_ts, lab_key}, min_ts, labels, value}
|
||||
end
|
||||
|
||||
defp delete_outdated(table, periods) do
|
||||
systime = System.system_time(:second)
|
||||
defp delete_outdated_series(table, periods, now) do
|
||||
maximum = Enum.reduce(periods, 0, fn {_, duration}, acc -> duration + acc end)
|
||||
|
||||
since = systime - maximum
|
||||
match = {{:_, :_, :"$1"}, :_, :_, :_}
|
||||
since = now - maximum
|
||||
match = {{:_, :"$1", :_}, :_, :_, :_}
|
||||
guard = [{:<, :"$1", since}]
|
||||
|
||||
:ets.select_delete(table, [{match, guard, [true]}])
|
||||
end
|
||||
|
||||
defp inner_store(table, series, value, labels, time) do
|
||||
key = {to_string(series), :erlang.phash2(labels), time}
|
||||
defp inner_store(series_table, latest_table, series, value, labels, time) do
|
||||
series = to_string(series)
|
||||
hash = :erlang.phash2(labels)
|
||||
key = {series, time, hash}
|
||||
|
||||
value =
|
||||
case :ets.lookup(table, key) do
|
||||
merged =
|
||||
case :ets.lookup(series_table, key) do
|
||||
[{_key, _time, _labels, old_value}] -> Value.union(old_value, value)
|
||||
_ -> value
|
||||
end
|
||||
|
||||
:ets.insert(table, {key, time, labels, value})
|
||||
:ets.insert(series_table, {key, time, labels, merged})
|
||||
|
||||
case :ets.lookup(latest_table, {series, hash}) do
|
||||
[{_, _, _, prev_time}] when prev_time > time ->
|
||||
:ok
|
||||
|
||||
[{_, _, prev_value, ^time}] ->
|
||||
:ets.insert(latest_table, {{series, hash}, labels, Value.union(prev_value, value), time})
|
||||
|
||||
_ ->
|
||||
:ets.insert(latest_table, {{series, hash}, labels, value, time})
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_outdated_latest(latest_table, periods, now) do
|
||||
maximum = Enum.reduce(periods, 0, fn {_, duration}, acc -> duration + acc end)
|
||||
since = now - maximum
|
||||
match = {{:_, :_}, :_, :_, :"$1"}
|
||||
guard = [{:<, :"$1", since}]
|
||||
|
||||
:ets.select_delete(latest_table, [{match, guard, [true]}])
|
||||
end
|
||||
|
||||
defp rebuild_latest(series_table, latest_table) do
|
||||
fun = fn {{series, max_ts, hash}, _min_ts, labels, value}, acc ->
|
||||
Map.update(acc, {series, hash}, {labels, value, max_ts}, fn
|
||||
{_, _, prev_ts} = entry when prev_ts >= max_ts -> entry
|
||||
_ -> {labels, value, max_ts}
|
||||
end)
|
||||
end
|
||||
|
||||
fun
|
||||
|> :ets.foldl(%{}, series_table)
|
||||
|> Enum.each(fn {key, {labels, value, max_ts}} ->
|
||||
case :ets.lookup(latest_table, key) do
|
||||
[{_, _, _, prev_ts}] when prev_ts >= max_ts -> :ok
|
||||
_ -> :ets.insert(latest_table, {key, labels, value, max_ts})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# Scheduling
|
||||
|
|
@ -370,13 +447,42 @@ defmodule Oban.Met.Recorder do
|
|||
# Fetching & Filtering
|
||||
|
||||
defp select(table, series, since, filters) do
|
||||
stime = System.system_time(:second)
|
||||
match = {{to_string(series), :_, :"$2"}, :_, :"$1", :_}
|
||||
guard = filters_to_guards(filters, {:>=, :"$2", stime - since})
|
||||
cutoff = System.system_time(:second) - since
|
||||
match = {{to_string(series), :_, :_}, :_, :"$1", :_}
|
||||
|
||||
:ets.select_reverse(table, [{match, [guard], [:"$_"]}])
|
||||
conditions =
|
||||
case filters do
|
||||
[_ | _] -> [filters_to_guards(filters, true)]
|
||||
_ -> []
|
||||
end
|
||||
|
||||
case :ets.select_reverse(table, [{match, conditions, [:"$_"]}], @chunk_size) do
|
||||
{rows, cont} -> collect_until(rows, cont, cutoff, [])
|
||||
:"$end_of_table" -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_until(rows, cont, cutoff, acc) do
|
||||
case take_recent(rows, cutoff, acc) do
|
||||
{:done, acc} ->
|
||||
acc
|
||||
|
||||
{:continue, acc} ->
|
||||
case :ets.select(cont) do
|
||||
{more, more_cont} -> collect_until(more, more_cont, cutoff, acc)
|
||||
:"$end_of_table" -> acc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp take_recent([], _cutoff, acc), do: {:continue, acc}
|
||||
|
||||
defp take_recent([{{_, max_ts, _}, _, _, _} = row | rest], cutoff, acc) when max_ts >= cutoff do
|
||||
take_recent(rest, cutoff, [row | acc])
|
||||
end
|
||||
|
||||
defp take_recent(_, _cutoff, acc), do: {:done, acc}
|
||||
|
||||
defp filters_to_guards(nil, base), do: base
|
||||
|
||||
defp filters_to_guards(filters, base) do
|
||||
|
|
|
|||
2
vendor/oban_met/mix.exs
vendored
|
|
@ -2,7 +2,7 @@ defmodule Oban.Met.MixProject do
|
|||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/oban-bg/oban_met"
|
||||
@version "1.1.0"
|
||||
@version "1.2.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
|
|
|||
BIN
vendor/oban_pro/.hex
vendored
4
vendor/oban_pro/lib/oban/pro/batch.ex
vendored
|
|
@ -500,8 +500,8 @@ defmodule Oban.Pro.Batch do
|
|||
Validation.validate_schema!(opts,
|
||||
batch_id: :string,
|
||||
batch_name: :string,
|
||||
callback_opts: :list,
|
||||
callback_worker: :module
|
||||
callback_opts: :any,
|
||||
callback_worker: {:or, [:atom, :string]}
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
42
vendor/oban_pro/lib/oban/pro/chunk.ex
vendored
|
|
@ -357,6 +357,48 @@ defmodule Oban.Pro.Chunk do
|
|||
Utils.hash64(chunk_data)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def repair_chunks(conf, opts) do
|
||||
limit = Keyword.fetch!(opts, :limit)
|
||||
timeout = Keyword.fetch!(opts, :timeout)
|
||||
|
||||
query =
|
||||
Job
|
||||
|> where([j], j.state in ~w(available scheduled retryable))
|
||||
|> where([j], fragment("? \\? 'chunk_by'", j.meta))
|
||||
|> where([j], fragment("NOT ? \\? 'chunk_id'", j.meta))
|
||||
|> limit(^limit)
|
||||
|
||||
conf
|
||||
|> Repo.all(query, timeout: timeout)
|
||||
|> Enum.group_by(&gen_chunk_id/1)
|
||||
|> Enum.each(fn {chunk_id, grouped_jobs} ->
|
||||
ids = Enum.map(grouped_jobs, & &1.id)
|
||||
|
||||
Job
|
||||
|> where([j], j.id in ^ids)
|
||||
|> update([j], set: [meta: fragment("jsonb_set(?, '{chunk_id}', ?)", j.meta, ^chunk_id)])
|
||||
|> then(&Repo.update_all(conf, &1, [], timeout: timeout))
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp gen_chunk_id(job) do
|
||||
chunk_by = normalize_by(job.meta["chunk_by"])
|
||||
|
||||
to_chunk_id(chunk_by, job.worker, job.args, queue: job.queue, meta: job.meta)
|
||||
end
|
||||
|
||||
defp normalize_by(by) do
|
||||
# This can't use Utils.chunk_by/1 because it has to cast to atom keys.
|
||||
Enum.map(by, fn
|
||||
"worker" -> :worker
|
||||
["args", keys] -> [:args, Enum.map(keys, &Utils.maybe_to_atom/1)]
|
||||
["meta", keys] -> [:meta, Enum.map(keys, &Utils.maybe_to_atom/1)]
|
||||
end)
|
||||
end
|
||||
|
||||
# Public Interface
|
||||
|
||||
@doc false
|
||||
|
|
|
|||
14
vendor/oban_pro/lib/oban/pro/diagnostics.ex
vendored
|
|
@ -59,7 +59,7 @@ defmodule Oban.Pro.Diagnostics do
|
|||
end
|
||||
end
|
||||
|
||||
case Enum.find_value(list_oban_instances(), finder) do
|
||||
case Enum.find_value(oban_instances(), finder) do
|
||||
{conf, pid} ->
|
||||
payload = %{"info" => process_info(pid), "job_id" => job_id, "node" => inspect(conf.node)}
|
||||
|
||||
|
|
@ -80,11 +80,11 @@ defmodule Oban.Pro.Diagnostics do
|
|||
|
||||
defp discover_and_subscribe(state) do
|
||||
state =
|
||||
Enum.reduce(list_oban_instances(), state, fn conf, acc ->
|
||||
Enum.reduce(oban_instances(), state, fn conf, acc ->
|
||||
if MapSet.member?(acc.subscribed, conf.name) do
|
||||
acc
|
||||
else
|
||||
:ok = Notifier.listen(conf.name, [:diagnostics])
|
||||
Notifier.listen(conf, [:diagnostics])
|
||||
|
||||
%{acc | subscribed: MapSet.put(acc.subscribed, conf.name)}
|
||||
end
|
||||
|
|
@ -95,12 +95,10 @@ defmodule Oban.Pro.Diagnostics do
|
|||
|> schedule_discover()
|
||||
end
|
||||
|
||||
defp list_oban_instances do
|
||||
match = [{{{:"$1", Oban.Notifier}, :_, :_}, [], [:"$1"]}]
|
||||
defp oban_instances do
|
||||
match = [{{:"$1", :_, :"$2"}, [{:not, {:is_tuple, :"$1"}}], [:"$2"]}]
|
||||
|
||||
Oban.Registry
|
||||
|> Registry.select(match)
|
||||
|> Enum.map(&Oban.config/1)
|
||||
Registry.select(Oban.Registry, match)
|
||||
end
|
||||
|
||||
defp process_info(pid) do
|
||||
|
|
|
|||
65
vendor/oban_pro/lib/oban/pro/engines/smart.ex
vendored
|
|
@ -549,9 +549,13 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
end
|
||||
end
|
||||
|
||||
defmacrop xact_lock(pref_key, lock_key) do
|
||||
defmacrop try_xact_lock(pref_key, lock_key) do
|
||||
quote do
|
||||
fragment("pg_try_advisory_xact_lock(?::int, ?::int)", unquote(pref_key), unquote(lock_key))
|
||||
fragment(
|
||||
"pg_try_advisory_xact_lock(?::int, ?::int)",
|
||||
unquote(pref_key),
|
||||
unquote(lock_key)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -750,6 +754,7 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
|> Multi.put(:conf, conf)
|
||||
|> Multi.put(:running, running)
|
||||
|> Multi.put(:producer, track_acks(acks, producer))
|
||||
|> Multi.run(:afh_locks, &take_flush_locks/2)
|
||||
|> Multi.run(:all_producers, &all_producers/2)
|
||||
|> Multi.run(:ack_ids, &ack_jobs/2)
|
||||
|> Multi.run(:afh_ids, &run_flush_handlers/2)
|
||||
|
|
@ -764,7 +769,7 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
{:ok, {producer, jobs}}
|
||||
|
||||
{:error, _op, %{postgres: %{code: :unique_violation, detail: detail}}, changes} ->
|
||||
clear_uniq_violation(changes.conf, detail)
|
||||
clear_uniq_violation(changes.conf, detail, ~w(available executing retryable))
|
||||
|
||||
retry_unique_violation(:fetch_jobs, [conf, producer, running])
|
||||
|
||||
|
|
@ -773,6 +778,11 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
|
||||
fetch_jobs(conf, producer, running)
|
||||
|
||||
{:error, _operation, %{postgres: %{code: :deadlock_detected}}, _changes} ->
|
||||
jittery_sleep()
|
||||
|
||||
fetch_jobs(conf, producer, running)
|
||||
|
||||
{:error, _operation, error, _changes} ->
|
||||
raise error
|
||||
end
|
||||
|
|
@ -781,6 +791,11 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
clear_uniq_violation(conf, detail, Enum.map(Job.states(), &to_string/1))
|
||||
|
||||
retry_unique_violation(:fetch_jobs, [conf, producer, running])
|
||||
|
||||
:error, %Postgrex.Error{postgres: %{code: :deadlock_detected}} ->
|
||||
jittery_sleep()
|
||||
|
||||
fetch_jobs(conf, producer, running)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
|
@ -1000,7 +1015,7 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
#
|
||||
# We find conflicting jobs by matching the unique key in meta and filtering by the expected
|
||||
# states. Clearing their `uniq_bmp` disables the unique constraint, allowing the transition.
|
||||
def clear_uniq_violation(%Config{} = conf, detail, states \\ ["executing"]) do
|
||||
def clear_uniq_violation(%Config{} = conf, detail, states) do
|
||||
# Match both column-based `(uniq_key)=(value)` and expression-based `((meta->>'uniq_key'))=(value)`
|
||||
# formats. The exact spacing is critical.
|
||||
pattern = ~r/\((?:uniq_key|\(meta ->> 'uniq_key'::text\))\)=\((?<key>.+)\)/
|
||||
|
|
@ -1025,7 +1040,7 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
if !:persistent_term.get(log_key, false) do
|
||||
:persistent_term.put(log_key, true)
|
||||
|
||||
Logger.info(fn ->
|
||||
Logger.warning(fn ->
|
||||
"""
|
||||
[Oban.Pro.Engines.Smart] Unique constraint violation repaired.
|
||||
|
||||
|
|
@ -1256,14 +1271,9 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
defp prepare_chains(_repo, %{link_map: link_map, conf: conf}) when map_size(link_map) > 0 do
|
||||
chain_ids = Map.keys(link_map)
|
||||
|
||||
if has_advisory_locks?(conf) do
|
||||
lock_keys =
|
||||
chain_ids
|
||||
|> Enum.map(&:erlang.phash2({conf.prefix, &1}))
|
||||
|> Enum.sort()
|
||||
|
||||
Repo.query!(conf, "SELECT pg_advisory_xact_lock(unnest($1::bigint[]))", [lock_keys])
|
||||
end
|
||||
chain_ids
|
||||
|> Enum.map(&:erlang.phash2({conf.prefix, :chain, &1}))
|
||||
|> take_advisory_locks(conf)
|
||||
|
||||
query =
|
||||
from(
|
||||
|
|
@ -1448,6 +1458,17 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
track_acks(acks, producer)
|
||||
end
|
||||
|
||||
defp take_flush_locks(_repo, %{acks: acks, conf: conf}) do
|
||||
acks
|
||||
|> Enum.flat_map(&elem(&1, 2))
|
||||
|> Enum.flat_map(fn
|
||||
{Oban.Pro.Workflow, _, [wids, _con]} when is_list(wids) -> wids
|
||||
_ -> []
|
||||
end)
|
||||
|> Enum.map(&:erlang.phash2({conf.prefix, :flush, &1}))
|
||||
|> take_advisory_locks(conf)
|
||||
end
|
||||
|
||||
defp run_flush_handlers(_repo, %{acks: acks, conf: conf}) do
|
||||
{:ok, run_flush_handlers(acks, conf)}
|
||||
end
|
||||
|
|
@ -1477,7 +1498,7 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
not has_advisory_locks?(conf) ->
|
||||
{:ok, Repo.all(conf, lock(query, "FOR UPDATE NOWAIT"))}
|
||||
|
||||
take_advisory_lock?(conf, queue) ->
|
||||
try_advisory_lock?(queue, conf) ->
|
||||
{:ok, Repo.all(conf, query)}
|
||||
|
||||
true ->
|
||||
|
|
@ -1514,15 +1535,27 @@ defmodule Oban.Pro.Engines.Smart do
|
|||
end)
|
||||
end
|
||||
|
||||
defp take_advisory_lock?(conf, queue) do
|
||||
defp try_advisory_lock?(queue, conf) do
|
||||
pre = :erlang.phash2(conf.prefix)
|
||||
key = :erlang.phash2(queue)
|
||||
|
||||
query = from(f in xact_lock(^pre, ^key), select: f.f0)
|
||||
query = from(f in try_xact_lock(^pre, ^key), select: f.f0)
|
||||
|
||||
Repo.one(conf, query)
|
||||
end
|
||||
|
||||
defp take_advisory_locks(keys, conf) do
|
||||
if Enum.any?(keys) and has_advisory_locks?(conf) do
|
||||
keys = :lists.usort(keys)
|
||||
|
||||
Repo.query!(conf, "SELECT pg_advisory_xact_lock(unnest($1::bigint[]))", [keys])
|
||||
|
||||
{:ok, length(keys)}
|
||||
else
|
||||
{:ok, 0}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_jobs(_repo, %{conf: conf, producer: producer} = changes) do
|
||||
span(:fetch, conf, %{queue: producer.queue}, fn ->
|
||||
subset_query = fetch_subquery(changes)
|
||||
|
|
|
|||
23
vendor/oban_pro/lib/oban/pro/limiters/global.ex
vendored
|
|
@ -37,28 +37,31 @@ defmodule Oban.Pro.Limiters.Global do
|
|||
end
|
||||
|
||||
defp separate_demands(conf, allowed, producer, tracked) do
|
||||
untracked =
|
||||
for key <- Partition.available_keys(conf, producer.queue, producer.meta.local_limit),
|
||||
not is_map_key(tracked, key),
|
||||
into: %{},
|
||||
do: {key, allowed}
|
||||
keys = Partition.available_keys(conf, producer.queue, producer.meta.local_limit)
|
||||
|
||||
allowed =
|
||||
if producer.meta.global_limit.burst do
|
||||
size =
|
||||
tracked
|
||||
|> Map.merge(untracked)
|
||||
|> Map.delete("none")
|
||||
|> map_size()
|
||||
keys
|
||||
|> MapSet.new()
|
||||
|> MapSet.union(MapSet.new(Map.keys(tracked)))
|
||||
|> MapSet.delete("none")
|
||||
|> MapSet.size()
|
||||
|> max(1)
|
||||
|
||||
producer.meta.local_limit
|
||||
(producer.meta.local_limit + size - 1)
|
||||
|> div(size)
|
||||
|> max(allowed)
|
||||
else
|
||||
allowed
|
||||
end
|
||||
|
||||
untracked =
|
||||
for key <- keys,
|
||||
not is_map_key(tracked, key),
|
||||
into: %{},
|
||||
do: {key, allowed}
|
||||
|
||||
Enum.reduce(tracked, untracked, fn {key, total}, acc ->
|
||||
case max(allowed - total, 0) do
|
||||
demand when demand > 0 -> Map.put(acc, key, demand)
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ defmodule Oban.Pro.Migrations.V170 do
|
|||
cancelled = GREATEST(0, wf.cancelled + COALESCE(d.cancelled, 0)),
|
||||
discarded = GREATEST(0, wf.discarded + COALESCE(d.discarded, 0)),
|
||||
started_at = CASE
|
||||
WHEN wf.started_at IS NULL AND d.executing > 0 THEN NOW()
|
||||
WHEN wf.started_at IS NULL AND d.executing > 0 THEN timezone('UTC', now())
|
||||
ELSE wf.started_at
|
||||
END,
|
||||
completed_at = CASE
|
||||
|
|
@ -76,7 +76,7 @@ defmodule Oban.Pro.Migrations.V170 do
|
|||
AND (wf.suspended + wf.available + wf.scheduled + wf.executing + wf.retryable
|
||||
+ COALESCE(d.suspended, 0) + COALESCE(d.available, 0) + COALESCE(d.scheduled, 0)
|
||||
+ COALESCE(d.executing, 0) + COALESCE(d.retryable, 0)) <= 0
|
||||
THEN NOW()
|
||||
THEN timezone('UTC', now())
|
||||
ELSE wf.completed_at
|
||||
END
|
||||
FROM (
|
||||
|
|
@ -159,17 +159,9 @@ defmodule Oban.Pro.Migrations.V170 do
|
|||
# enables querying meta fields directly without relying on generated columns. Partitioned
|
||||
# tables already use expression indexes from v1.5.0.
|
||||
if not opts.generated_columns and not partitioned?(opts) do
|
||||
drop_if_exists index(:oban_jobs, [],
|
||||
name: :oban_jobs_unique_index,
|
||||
concurrently: opts.concurrently,
|
||||
prefix: opts.prefix
|
||||
)
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_unique_index RENAME TO oban_jobs_unique_index_old"
|
||||
|
||||
drop_if_exists index(:oban_jobs, [],
|
||||
name: :oban_jobs_partition_index,
|
||||
concurrently: opts.concurrently,
|
||||
prefix: opts.prefix
|
||||
)
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_partition_index RENAME TO oban_jobs_partition_index_old"
|
||||
|
||||
create_if_not_exists index(
|
||||
:oban_jobs,
|
||||
|
|
@ -238,17 +230,9 @@ defmodule Oban.Pro.Migrations.V170 do
|
|||
|
||||
# Replace the non-partial state timestamp indexes from core Oban with partial indexes.
|
||||
# These partial indexes are smaller and more efficient for pruning operations.
|
||||
drop_if_exists index(:oban_jobs, [],
|
||||
name: :oban_jobs_state_cancelled_at_index,
|
||||
concurrently: opts.concurrently,
|
||||
prefix: opts.prefix
|
||||
)
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_state_cancelled_at_index RENAME TO oban_jobs_cancelled_at_index_old"
|
||||
|
||||
drop_if_exists index(:oban_jobs, [],
|
||||
name: :oban_jobs_state_discarded_at_index,
|
||||
concurrently: opts.concurrently,
|
||||
prefix: opts.prefix
|
||||
)
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_state_discarded_at_index RENAME TO oban_jobs_discarded_at_index_old"
|
||||
|
||||
create_if_not_exists index(
|
||||
:oban_jobs,
|
||||
|
|
@ -390,27 +374,33 @@ defmodule Oban.Pro.Migrations.V170 do
|
|||
prefix: opts.prefix
|
||||
)
|
||||
|
||||
create_if_not_exists index(
|
||||
:oban_jobs,
|
||||
[:state, :cancelled_at],
|
||||
concurrently: opts.concurrently,
|
||||
name: :oban_jobs_state_cancelled_at_index,
|
||||
prefix: opts.prefix
|
||||
)
|
||||
|
||||
create_if_not_exists index(
|
||||
:oban_jobs,
|
||||
[:state, :discarded_at],
|
||||
concurrently: opts.concurrently,
|
||||
name: :oban_jobs_state_discarded_at_index,
|
||||
prefix: opts.prefix
|
||||
)
|
||||
|
||||
# Rename old indexes back
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_workflow_index_old RENAME TO oban_jobs_workflow_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_sup_workflow_index_old RENAME TO oban_jobs_sup_workflow_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_chain_index_old RENAME TO oban_jobs_chain_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_cancelled_at_index_old RENAME TO oban_jobs_state_cancelled_at_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_discarded_at_index_old RENAME TO oban_jobs_state_discarded_at_index"
|
||||
|
||||
if not opts.generated_columns and not partitioned?(opts) do
|
||||
drop_if_exists index(:oban_jobs, [],
|
||||
name: :oban_jobs_unique_index,
|
||||
concurrently: opts.concurrently,
|
||||
prefix: opts.prefix
|
||||
)
|
||||
|
||||
drop_if_exists index(:oban_jobs, [],
|
||||
name: :oban_jobs_partition_index,
|
||||
concurrently: opts.concurrently,
|
||||
prefix: opts.prefix
|
||||
)
|
||||
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_unique_index_old RENAME TO oban_jobs_unique_index"
|
||||
|
||||
execute "ALTER INDEX IF EXISTS #{opts.quoted}.oban_jobs_partition_index_old RENAME TO oban_jobs_partition_index"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
62
vendor/oban_pro/lib/oban/pro/partition.ex
vendored
|
|
@ -15,6 +15,16 @@ defmodule Oban.Pro.Partition do
|
|||
@conf_tab Module.concat(__MODULE__, Conf)
|
||||
@keys_tab Module.concat(__MODULE__, Keys)
|
||||
|
||||
defmacrop coalesce_partition(meta) do
|
||||
quote do
|
||||
fragment(
|
||||
"COALESCE(?->'global_limit'->'partition', ?->'rate_limit'->'partition')",
|
||||
unquote(meta),
|
||||
unquote(meta)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@impl Oban.Pro.Handler
|
||||
def on_start do
|
||||
:ets.new(@conf_tab, [:set, :named_table, :public, read_concurrency: true])
|
||||
|
|
@ -131,6 +141,58 @@ defmodule Oban.Pro.Partition do
|
|||
end
|
||||
end
|
||||
|
||||
# Rescuing
|
||||
|
||||
@doc false
|
||||
def repair_partitions(conf, opts) do
|
||||
limit = Keyword.fetch!(opts, :limit)
|
||||
timeout = Keyword.fetch!(opts, :timeout)
|
||||
|
||||
for {queue, partition} <- partitioned_queues(conf, timeout) do
|
||||
query =
|
||||
Job
|
||||
|> where([j], j.queue == ^queue)
|
||||
|> where([j], j.state in ~w(available scheduled retryable))
|
||||
|> where([j], fragment("NOT ? \\? 'partition_key'", j.meta))
|
||||
|> limit(^limit)
|
||||
|
||||
conf
|
||||
|> Repo.all(query, timeout: timeout)
|
||||
|> Enum.group_by(fn job -> gen_partition_key(job, partition) end)
|
||||
|> Enum.each(fn {key, grouped_jobs} ->
|
||||
ids = Enum.map(grouped_jobs, & &1.id)
|
||||
|
||||
Job
|
||||
|> where([j], j.id in ^ids)
|
||||
|> update([j], set: [meta: fragment("jsonb_set(?, '{partition_key}', ?)", j.meta, ^key)])
|
||||
|> then(&Repo.update_all(conf, &1, [], timeout: timeout))
|
||||
end)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp gen_partition_key(job, partition) do
|
||||
job
|
||||
|> Ecto.Changeset.change()
|
||||
|> gen_key(partition)
|
||||
end
|
||||
|
||||
defp partitioned_queues(conf, timeout) do
|
||||
normalize_partition = fn partition ->
|
||||
fields = partition |> Map.get("fields", []) |> Enum.map(&String.to_existing_atom/1)
|
||||
|
||||
%{fields: fields, keys: Map.get(partition, "keys", [])}
|
||||
end
|
||||
|
||||
Producer
|
||||
|> where([p], fragment("jsonb_typeof(?) = 'object'", coalesce_partition(p.meta)))
|
||||
|> select([p], {p.queue, coalesce_partition(p.meta)})
|
||||
|> distinct(true)
|
||||
|> then(&Repo.all(conf, &1, timeout: timeout))
|
||||
|> Enum.map(fn {queue, partition} -> {queue, normalize_partition.(partition)} end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
We can't guarantee that the queue is running locally, running on a current node, or running at
|
||||
all. This will check the following locations:
|
||||
|
|
|
|||
|
|
@ -150,6 +150,16 @@ defmodule Oban.Pro.Plugins.DynamicCron do
|
|||
expression inserts a job once each time the plugin starts and won't re-fire until the next
|
||||
restart.
|
||||
|
||||
## Job Metadata
|
||||
|
||||
Scheduled jobs receive automatic metadata identifying the entry and the slot it fired for:
|
||||
|
||||
* `cron` - always `true`
|
||||
* `cron_at` - the slot time in UTC, stable across retries and guaranteed catch-ups
|
||||
* `cron_expr` - the cron expression
|
||||
* `cron_name` - the entry's `:name`
|
||||
* `cron_tz` - the timezone the expression evaluates against
|
||||
|
||||
## Automatic Synchronization
|
||||
|
||||
Synchronizing persisted entries manually requires two deploys: one to flag it with `delete:
|
||||
|
|
@ -244,6 +254,11 @@ defmodule Oban.Pro.Plugins.DynamicCron do
|
|||
typical CRUD operations. The `DynamicCron` plugin provides convenience functions to simplify
|
||||
those operations.
|
||||
|
||||
> #### Treat Expressions as Trusted Input {: .warning}
|
||||
>
|
||||
> Cron expressions passed to `insert/2` and `update/3` are treated as trusted input.
|
||||
> Validate or constrain any user-provided expressions before passing them along.
|
||||
|
||||
The `insert/1` function takes a list of one or more tuples with the same `{expression, worker}`
|
||||
or `{expression, worker, options}` format as the plugin's `crontab` option:
|
||||
|
||||
|
|
@ -697,7 +712,20 @@ defmodule Oban.Pro.Plugins.DynamicCron do
|
|||
{args, opts} = Keyword.pop(entry.opts, :args, %{})
|
||||
{tz, opts} = Keyword.pop(opts, :timezone, state.timezone)
|
||||
|
||||
meta = %{cron: true, cron_expr: entry.expression, cron_name: entry.name, cron_tz: tz}
|
||||
# Add a minute so last_at's internal -1 minute and second truncation don't
|
||||
# snap an on-time slot back to the previous match.
|
||||
cron_at =
|
||||
entry.parsed
|
||||
|> Expression.last_at(DateTime.add(DateTime.now!(tz), 1, :minute))
|
||||
|> DateTime.shift_zone!("Etc/UTC")
|
||||
|
||||
meta = %{
|
||||
cron: true,
|
||||
cron_at: cron_at,
|
||||
cron_expr: entry.expression,
|
||||
cron_name: entry.name,
|
||||
cron_tz: tz
|
||||
}
|
||||
|
||||
opts =
|
||||
entry.worker.__opts__()
|
||||
|
|
|
|||
|
|
@ -28,8 +28,9 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
|
|||
|
||||
## Options
|
||||
|
||||
* `:repair_limit` — the maximum number of jobs each repair operation (workflows, chains,
|
||||
partitions, chunks) will process per cycle. Defaults to `1000`.
|
||||
* `:repair_limit` — the maximum number of jobs each partition or chunk repair will process per
|
||||
cycle. Defaults to `1000`. Workflow and chain repairs aren't subject to this limit—they're
|
||||
naturally bounded by the set of stuck compositions.
|
||||
|
||||
* `:retry_exhausted` — when `true`, jobs that have exhausted their attempts are rescued back to
|
||||
`available` with an incremented `max_attempts` instead of being discarded. Defaults to
|
||||
|
|
@ -43,20 +44,20 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
|
|||
The plugin automatically performs several repair operations during each rescue cycle to keep
|
||||
workflows, chains, partitions, and chunks healthy.
|
||||
|
||||
* **Workflow repair** — Jobs held waiting for dependencies that were deleted or missed a
|
||||
scheduling event are released. This handles edge cases where workflow jobs get stuck due to
|
||||
incomplete dependency resolution.
|
||||
* **Chain repair** — Chain jobs waiting on deleted or stuck predecessors are released to
|
||||
continue processing.
|
||||
|
||||
* **Chain repair** — Similar to workflows, chain jobs waiting on deleted or stuck predecessors
|
||||
are released to continue processing.
|
||||
* **Chunk repair** — Chunk jobs that are missing a `chunk_id` in their metadata have their
|
||||
chunk ID computed from the job's `chunk_by` configuration and set automatically. This can
|
||||
happen when chunk settings are changed after jobs are already enqueued.
|
||||
|
||||
* **Partition repair** — Jobs in partitioned queues that are missing a `partition_key` in their
|
||||
metadata (e.g., jobs scheduled before a queue was partitioned) have their partition key
|
||||
computed and set automatically.
|
||||
|
||||
* **Chunk repair** — Chunk jobs that are missing a `chunk_id` in their metadata have their
|
||||
chunk ID computed from the job's `chunk_by` configuration and set automatically. This can
|
||||
happen when chunk settings are changed after jobs are already enqueued.
|
||||
* **Workflow repair** — Jobs held waiting for dependencies that were deleted or missed a
|
||||
scheduling event are released. This handles edge cases where workflow jobs get stuck due to
|
||||
incomplete dependency resolution.
|
||||
|
||||
Each repair operation processes up to `repair_limit` jobs per cycle.
|
||||
|
||||
|
|
@ -111,7 +112,7 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
|
|||
alias Oban.{Job, Peer, Repo, Validation}
|
||||
alias Oban.Pro.Chunk
|
||||
alias Oban.Pro.Engines.Smart
|
||||
alias Oban.Pro.{Partition, Producer, Utils, Workflow}
|
||||
alias Oban.Pro.{Partition, Producer, Workflow}
|
||||
alias Oban.Pro.Stages.{Chain, Hooks}
|
||||
|
||||
require Logger
|
||||
|
|
@ -144,16 +145,6 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
|
|||
end
|
||||
end
|
||||
|
||||
defmacrop coalesce_partition(meta) do
|
||||
quote do
|
||||
fragment(
|
||||
"COALESCE(?->'global_limit'->'partition', ?->'rate_limit'->'partition')",
|
||||
unquote(meta),
|
||||
unquote(meta)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop track_rescued(meta) do
|
||||
quote do
|
||||
fragment(
|
||||
|
|
@ -233,10 +224,10 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
|
|||
:telemetry.span([:oban, :plugin], meta, fn ->
|
||||
orphan_meta = rescue_orphaned(state)
|
||||
|
||||
:ok = repair_workflows(state)
|
||||
:ok = repair_chains(state)
|
||||
:ok = repair_chunks(state)
|
||||
:ok = repair_partitions(state)
|
||||
:ok = repair_chunk_ids(state)
|
||||
:ok = repair_workflows(state)
|
||||
|
||||
{:ok, Map.merge(meta, orphan_meta)}
|
||||
end)
|
||||
|
|
@ -316,7 +307,7 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
|
|||
Repo.update_all(state.conf, query, [], timeout: state.timeout)
|
||||
catch
|
||||
:error, %Postgrex.Error{postgres: %{code: :unique_violation, detail: detail}} ->
|
||||
Smart.clear_uniq_violation(state.conf, detail)
|
||||
Smart.clear_uniq_violation(state.conf, detail, ~w(available executing retryable))
|
||||
|
||||
{0, []}
|
||||
end
|
||||
|
|
@ -333,7 +324,7 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
|
|||
Repo.update_all(state.conf, query, [], timeout: state.timeout)
|
||||
catch
|
||||
:error, %Postgrex.Error{postgres: %{code: :unique_violation, detail: detail}} ->
|
||||
Smart.clear_uniq_violation(state.conf, detail)
|
||||
Smart.clear_uniq_violation(state.conf, detail, ~w(available executing retryable))
|
||||
|
||||
{0, []}
|
||||
end
|
||||
|
|
@ -360,100 +351,21 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
|
|||
{count, partial_jobs}
|
||||
end
|
||||
|
||||
# Composition Repair
|
||||
|
||||
defp repair_workflows(state) do
|
||||
Workflow.rescue_workflows(state.conf, timeout: state.timeout, limit: state.repair_limit)
|
||||
end
|
||||
# Repair
|
||||
|
||||
defp repair_chains(state) do
|
||||
Chain.rescue_chains(state.conf, timeout: state.timeout, limit: state.repair_limit)
|
||||
Chain.repair_chains(state.conf, timeout: state.timeout)
|
||||
end
|
||||
|
||||
# Partition Repair
|
||||
defp repair_chunks(state) do
|
||||
Chunk.repair_chunks(state.conf, limit: state.repair_limit, timeout: state.timeout)
|
||||
end
|
||||
|
||||
defp repair_partitions(state) do
|
||||
for {queue, partition} <- partitioned_queues(state) do
|
||||
query =
|
||||
Job
|
||||
|> where([j], j.queue == ^queue)
|
||||
|> where([j], j.state in ~w(available scheduled retryable))
|
||||
|> where([j], fragment("NOT ? \\? 'partition_key'", j.meta))
|
||||
|> limit(^state.repair_limit)
|
||||
|
||||
state.conf
|
||||
|> Repo.all(query, timeout: state.timeout)
|
||||
|> Enum.group_by(fn job -> gen_partition_key(job, partition) end)
|
||||
|> Enum.each(fn {key, grouped_jobs} ->
|
||||
ids = Enum.map(grouped_jobs, & &1.id)
|
||||
|
||||
Job
|
||||
|> where([j], j.id in ^ids)
|
||||
|> update([j], set: [meta: fragment("jsonb_set(?, '{partition_key}', ?)", j.meta, ^key)])
|
||||
|> then(&Repo.update_all(state.conf, &1, [], timeout: state.timeout))
|
||||
end)
|
||||
end
|
||||
|
||||
:ok
|
||||
Partition.repair_partitions(state.conf, limit: state.repair_limit, timeout: state.timeout)
|
||||
end
|
||||
|
||||
defp gen_partition_key(job, partition) do
|
||||
job
|
||||
|> Ecto.Changeset.change()
|
||||
|> Partition.gen_key(partition)
|
||||
end
|
||||
|
||||
defp partitioned_queues(state) do
|
||||
normalize_partition = fn partition ->
|
||||
fields = partition |> Map.get("fields", []) |> Enum.map(&String.to_existing_atom/1)
|
||||
|
||||
%{fields: fields, keys: Map.get(partition, "keys", [])}
|
||||
end
|
||||
|
||||
Producer
|
||||
|> where([p], fragment("jsonb_typeof(?) = 'object'", coalesce_partition(p.meta)))
|
||||
|> select([p], {p.queue, coalesce_partition(p.meta)})
|
||||
|> distinct(true)
|
||||
|> then(&Repo.all(state.conf, &1, timeout: state.timeout))
|
||||
|> Enum.map(fn {queue, partition} -> {queue, normalize_partition.(partition)} end)
|
||||
end
|
||||
|
||||
# Chunk Repair
|
||||
|
||||
defp repair_chunk_ids(state) do
|
||||
query =
|
||||
Job
|
||||
|> where([j], j.state in ~w(available scheduled retryable))
|
||||
|> where([j], fragment("? \\? 'chunk_by'", j.meta))
|
||||
|> where([j], fragment("NOT ? \\? 'chunk_id'", j.meta))
|
||||
|> limit(^state.repair_limit)
|
||||
|
||||
state.conf
|
||||
|> Repo.all(query, timeout: state.timeout)
|
||||
|> Enum.group_by(&gen_chunk_id/1)
|
||||
|> Enum.each(fn {chunk_id, grouped_jobs} ->
|
||||
ids = Enum.map(grouped_jobs, & &1.id)
|
||||
|
||||
Job
|
||||
|> where([j], j.id in ^ids)
|
||||
|> update([j], set: [meta: fragment("jsonb_set(?, '{chunk_id}', ?)", j.meta, ^chunk_id)])
|
||||
|> then(&Repo.update_all(state.conf, &1, [], timeout: state.timeout))
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp gen_chunk_id(job) do
|
||||
chunk_by = normalize_chunk_by(job.meta["chunk_by"])
|
||||
|
||||
Chunk.to_chunk_id(chunk_by, job.worker, job.args, queue: job.queue, meta: job.meta)
|
||||
end
|
||||
|
||||
defp normalize_chunk_by(chunk_by) do
|
||||
Enum.map(chunk_by, fn
|
||||
"worker" -> :worker
|
||||
["args", keys] -> [:args, Enum.map(keys, &Utils.maybe_to_atom/1)]
|
||||
["meta", keys] -> [:meta, Enum.map(keys, &Utils.maybe_to_atom/1)]
|
||||
end)
|
||||
defp repair_workflows(state) do
|
||||
Workflow.rescue_workflows(state.conf, timeout: state.timeout)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
24
vendor/oban_pro/lib/oban/pro/stages/chain.ex
vendored
|
|
@ -82,7 +82,7 @@ defmodule Oban.Pro.Stages.Chain do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def rescue_chains(conf, opts \\ []) do
|
||||
def repair_chains(conf, opts \\ []) do
|
||||
prev_query =
|
||||
Job
|
||||
|> where([j], j.state in ~w(available suspended scheduled retryable executing))
|
||||
|
|
@ -98,10 +98,8 @@ defmodule Oban.Pro.Stages.Chain do
|
|||
|> limit(1)
|
||||
|
||||
subquery =
|
||||
if Utils.has_legacy_suspended?(conf) do
|
||||
subquery
|
||||
|> where([j], j.state == "scheduled")
|
||||
|> where([j], fragment("?->>'on_hold' = 'true'", j.meta))
|
||||
if Utils.has_legacy_chains?(conf) do
|
||||
legacy_suspended?(subquery)
|
||||
else
|
||||
where(subquery, [j], j.state == "suspended")
|
||||
end
|
||||
|
|
@ -183,16 +181,15 @@ defmodule Oban.Pro.Stages.Chain do
|
|||
subquery =
|
||||
Job
|
||||
|> select([j], j.id)
|
||||
|> where([j], fragment("? \\? 'chain_id'", j.meta))
|
||||
|> where([j], fragment("?->>'chain_id'", j.meta) == ^chain_id)
|
||||
|> order_by(asc: :id)
|
||||
|> limit(1)
|
||||
|> lock("FOR UPDATE")
|
||||
|
||||
subquery =
|
||||
if Utils.has_legacy_suspended?(conf) do
|
||||
subquery
|
||||
|> where([j], j.state == "scheduled")
|
||||
|> where([j], fragment("?->>'on_hold' = 'true'", j.meta))
|
||||
if Utils.has_legacy_chains?(conf) do
|
||||
legacy_suspended?(subquery)
|
||||
else
|
||||
where(subquery, [j], j.state == "suspended")
|
||||
end
|
||||
|
|
@ -216,6 +213,15 @@ defmodule Oban.Pro.Stages.Chain do
|
|||
|
||||
# Helpers
|
||||
|
||||
defp legacy_suspended?(subquery) do
|
||||
where(
|
||||
subquery,
|
||||
[j],
|
||||
j.state == "suspended" or
|
||||
(j.state == "scheduled" and fragment("?->>'on_hold' = 'true'", j.meta))
|
||||
)
|
||||
end
|
||||
|
||||
defp merge_conf_opts(conf, opts) do
|
||||
case Keyword.fetch(opts, :chain) do
|
||||
{:ok, chain} ->
|
||||
|
|
|
|||
6
vendor/oban_pro/lib/oban/pro/stages/hooks.ex
vendored
|
|
@ -71,6 +71,10 @@ defmodule Oban.Pro.Stages.Hooks do
|
|||
end
|
||||
end
|
||||
|
||||
def clear_hooks do
|
||||
:persistent_term.put(:oban_pro_hooks, [])
|
||||
end
|
||||
|
||||
# The `Workflow.after_cancelled` hook is deprecated and will be removed in a future version.
|
||||
def execute_after_cancelled(reason, job) do
|
||||
with {worker, job, opts} <- resolve_processing(job) do
|
||||
|
|
@ -93,6 +97,8 @@ defmodule Oban.Pro.Stages.Hooks do
|
|||
|
||||
def handle_event([:oban, :job, _], _timing, meta, _conf) do
|
||||
with {worker, job, opts} <- resolve_processing(meta.job) do
|
||||
job = %{job | unsaved_error: meta.job.unsaved_error}
|
||||
|
||||
execute_hook(worker, opts, :after_process, [to_hook_state(meta.state), job, meta[:result]])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
64
vendor/oban_pro/lib/oban/pro/utils.ex
vendored
|
|
@ -1,8 +1,12 @@
|
|||
defmodule Oban.Pro.Utils do
|
||||
@moduledoc false
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Ecto.Changeset
|
||||
|
||||
@legacy_ttl :timer.minutes(5)
|
||||
|
||||
@spec encode64(term(), [:compressed | :deterministic]) :: String.t()
|
||||
def encode64(term, opts \\ [:compressed]) do
|
||||
term
|
||||
|
|
@ -24,38 +28,50 @@ defmodule Oban.Pro.Utils do
|
|||
|> Base.encode64(padding: false)
|
||||
end
|
||||
|
||||
@spec persistent_cache(tuple(), fun()) :: any()
|
||||
@spec persistent_cache(tuple(), (-> any())) :: any()
|
||||
def persistent_cache(key, fun) when is_function(fun, 0) do
|
||||
persistent_cache(key, :infinity, fun)
|
||||
end
|
||||
|
||||
@spec persistent_cache(tuple(), timeout(), (-> any())) :: any()
|
||||
def persistent_cache(key, ttl, fun) when is_function(fun, 0) do
|
||||
now = System.monotonic_time(:millisecond)
|
||||
exp = if ttl == :infinity, do: :infinity, else: now + ttl
|
||||
|
||||
case :persistent_term.get(key, nil) do
|
||||
nil -> tap(fun.(), &:persistent_term.put(key, &1))
|
||||
val -> val
|
||||
{val, :infinity} -> val
|
||||
{val, expires_at} when expires_at > now -> val
|
||||
_ -> tap(fun.(), &:persistent_term.put(key, {&1, exp}))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if any legacy suspended jobs exist (scheduled + on_hold pattern).
|
||||
|
||||
Result is cached in persistent_term per prefix and only checked once per application lifetime.
|
||||
"""
|
||||
@spec has_legacy_suspended?(Oban.Config.t()) :: boolean()
|
||||
def has_legacy_suspended?(conf) do
|
||||
import Ecto.Query
|
||||
|
||||
persistent_cache({__MODULE__, :legacy_suspended, conf.prefix}, fn ->
|
||||
query =
|
||||
from(job in Oban.Job,
|
||||
where: fragment("? \\? 'workflow_id'", job.meta),
|
||||
where: fragment("? ->> 'workflow_id' IS NOT NULL", job.meta),
|
||||
where: job.state == "scheduled",
|
||||
where: fragment("? ->> 'on_hold' = 'true'", job.meta),
|
||||
select: true,
|
||||
limit: 1
|
||||
)
|
||||
|
||||
Oban.Repo.exists?(conf, query)
|
||||
@spec has_legacy_workflows?(Oban.Config.t()) :: boolean()
|
||||
def has_legacy_workflows?(conf) do
|
||||
persistent_cache({__MODULE__, :legacy_workflows, conf.name}, @legacy_ttl, fn ->
|
||||
has_legacy?(conf, "workflow_id")
|
||||
end)
|
||||
end
|
||||
|
||||
@spec has_legacy_chains?(Oban.Config.t()) :: boolean()
|
||||
def has_legacy_chains?(conf) do
|
||||
persistent_cache({__MODULE__, :legacy_chains, conf.name}, @legacy_ttl, fn ->
|
||||
has_legacy?(conf, "chain_id")
|
||||
end)
|
||||
end
|
||||
|
||||
defp has_legacy?(conf, key) do
|
||||
query =
|
||||
from(job in Oban.Job,
|
||||
where: fragment("? \\? ?", job.meta, ^key),
|
||||
where: job.state == "scheduled",
|
||||
where: fragment("? ->> 'on_hold' = 'true'", job.meta),
|
||||
select: true,
|
||||
limit: 1
|
||||
)
|
||||
|
||||
Oban.Repo.exists?(conf, query)
|
||||
end
|
||||
|
||||
@spec validate_opts(list(), fun()) :: :ok | {:error, term()}
|
||||
def validate_opts(opts, validator) do
|
||||
Enum.reduce_while(opts, :ok, fn opt, acc ->
|
||||
|
|
|
|||
64
vendor/oban_pro/lib/oban/pro/worker.ex
vendored
|
|
@ -18,6 +18,8 @@ defmodule Oban.Pro.Worker do
|
|||
worker name to continue without exceptions using the new worker code.
|
||||
* [🪝 Worker Hooks](#module-worker-hooks) — callbacks triggered around job execution, defined as
|
||||
callback functions on the worker, or in a separate module for reuse.
|
||||
* [📡 Awaiting Signals](#module-awaiting-signals) — pause mid-execution and resume when a signal
|
||||
delivers an external decision from anywhere in your app.
|
||||
* [⚖️ Rate Limiting Weight](#module-rate-limiting-weight) — assign custom weights to jobs to
|
||||
control the rate limit quota consumed by a job.
|
||||
|
||||
|
|
@ -702,6 +704,51 @@ defmodule Oban.Pro.Worker do
|
|||
These callbacks can also be defined in shared hook modules and attached explicitly or globally,
|
||||
just like execution hooks.
|
||||
|
||||
## Awaiting Signals
|
||||
|
||||
Jobs can pause mid-execution to wait for an external decision and resume when a signal arrives.
|
||||
This turns long-running workers into durable state machines that wait for human approval,
|
||||
third-party callbacks, or any other out-of-band event without holding a worker process or
|
||||
database connection open.
|
||||
|
||||
Pause a job with `await_signal/1` from inside `process/1` and resume it with `signal/2,3` from
|
||||
anywhere. For example, wait for approval before charging a credit card:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Approval do
|
||||
use Oban.Pro.Worker
|
||||
|
||||
@impl Oban.Pro.Worker
|
||||
def process(_job) do
|
||||
case Oban.Pro.Worker.await_signal(wait_for: {24, :hours}) do
|
||||
{:ok, %{decision: "approved"}} -> charge_card()
|
||||
{:ok, %{decision: "rejected"}} -> {:cancel, :rejected}
|
||||
{:error, :timeout} -> {:cancel, :no_decision}
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Then signal the job by id when the decision arrives:
|
||||
|
||||
```elixir
|
||||
Oban.Pro.Worker.signal(job_id, %{decision: "approved"})
|
||||
```
|
||||
|
||||
A list of job ids can be used for multiple jobs at once:
|
||||
|
||||
```elixir
|
||||
Oban.Pro.Worker.signal([job_a_id, job_b_id], %{decision: "approved"})
|
||||
```
|
||||
|
||||
A short live wait (`:wait_timeout`, default 5s) blocks during execution so signals that arrive
|
||||
immediately resume in place. Beyond that window, the job snoozes until either a signal arrives
|
||||
or the overall `:wait_for` deadline elapses, freeing the worker process during the wait. Signals
|
||||
are persisted, so one delivered before `await_signal/1` is reached is consumed on the next call.
|
||||
|
||||
Payloads may be any native term, as they're encoded before storage and decoded on retrieval, so
|
||||
atoms, tuples, and structs round-trip intact.
|
||||
|
||||
## Rate Limiting Weight
|
||||
|
||||
When using rate limiting with the Smart engine, each job consumes one unit of rate limit
|
||||
|
|
@ -761,7 +808,7 @@ defmodule Oban.Pro.Worker do
|
|||
import Ecto.Query, only: [where: 3, update: 3]
|
||||
|
||||
alias Oban.{Job, Notifier, Period, Repo, Validation, Worker}
|
||||
alias Oban.Pro.Batch
|
||||
alias Oban.Pro.{Batch, Utils}
|
||||
alias Oban.Pro.Stages.{Chain, Deadline, Encrypted, Executing, Hooks, Rate, Recorded, Structured}
|
||||
|
||||
@typedoc """
|
||||
|
|
@ -1385,9 +1432,9 @@ defmodule Oban.Pro.Worker do
|
|||
@doc """
|
||||
Deliver a signal to one or more jobs by id.
|
||||
|
||||
The `payload` is merged into the target jobs' `meta` under the `"signal"` key. If a job is
|
||||
parked inside `await_signal/2`, its `scheduled_at` is bumped to `now()` so it resumes
|
||||
immediately.
|
||||
The `payload` may be any term, since it's encoded before storage and decoded by
|
||||
`await_signal/1`, so atoms, tuples, and structs stay intact. If a job was parked by an
|
||||
`await_signal/1` timeout, it is made available to execute immediately.
|
||||
|
||||
Signals are fire-and-forget. Delivery to a job in a terminal state, or a job that doesn't
|
||||
exist, is a no-op and still returns `:ok`. When multiple signals arrive for the same target
|
||||
|
|
@ -1426,6 +1473,7 @@ defmodule Oban.Pro.Worker do
|
|||
conf = Oban.config(oban_name)
|
||||
now = DateTime.utc_now()
|
||||
ids = List.wrap(id_or_ids)
|
||||
encoded = Utils.encode64(payload)
|
||||
|
||||
query =
|
||||
Job
|
||||
|
|
@ -1433,7 +1481,7 @@ defmodule Oban.Pro.Worker do
|
|||
|> where([j], j.state in ~w(suspended available scheduled executing retryable))
|
||||
|> update([j],
|
||||
set: [
|
||||
meta: fragment("jsonb_set(?, '{signal}', ?::jsonb)", j.meta, ^payload),
|
||||
meta: fragment("jsonb_set(?, '{signal}', to_jsonb(?::text))", j.meta, ^encoded),
|
||||
scheduled_at: maybe_schedule(j, ^now)
|
||||
]
|
||||
)
|
||||
|
|
@ -1441,7 +1489,7 @@ defmodule Oban.Pro.Worker do
|
|||
Repo.transaction(conf, fn ->
|
||||
Repo.update_all(conf, query, [])
|
||||
|
||||
Notifier.notify(conf, :pro_signal, %{job_ids: ids, signal: payload})
|
||||
Notifier.notify(conf, :pro_signal, %{job_ids: ids, signal: encoded})
|
||||
end)
|
||||
|
||||
:ok
|
||||
|
|
@ -1488,7 +1536,7 @@ defmodule Oban.Pro.Worker do
|
|||
end)
|
||||
|
||||
cond do
|
||||
payload = meta["signal"] -> {:ok, payload}
|
||||
encoded = meta["signal"] -> {:ok, Utils.decode64(encoded)}
|
||||
past_deadline?(wait_until) -> {:error, :timeout}
|
||||
true -> await_signal(conf.name, id, wait_until, wait_timeout)
|
||||
end
|
||||
|
|
@ -1514,7 +1562,7 @@ defmodule Oban.Pro.Worker do
|
|||
receive do
|
||||
{:notification, :pro_signal, %{"job_ids" => ids, "signal" => signal}} ->
|
||||
if id in ids do
|
||||
{:ok, signal}
|
||||
{:ok, Utils.decode64(signal)}
|
||||
else
|
||||
await_loop(id, wait_until, wait_timeout)
|
||||
end
|
||||
|
|
|
|||
55
vendor/oban_pro/lib/oban/pro/workflow.ex
vendored
|
|
@ -469,8 +469,8 @@ defmodule Oban.Pro.Workflow do
|
|||
@impl Oban.Pro.Worker
|
||||
def process(_job) do
|
||||
case Oban.Pro.Worker.await_signal(wait_for: {24, :hours}) do
|
||||
{:ok, %{"decision" => "approved"}} -> charge_card()
|
||||
{:ok, %{"decision" => "rejected"}} -> {:cancel, :rejected}
|
||||
{:ok, %{decision: "approved"}} -> charge_card()
|
||||
{:ok, %{decision: "rejected"}} -> {:cancel, :rejected}
|
||||
{:error, :timeout} -> {:cancel, :no_decision}
|
||||
end
|
||||
end
|
||||
|
|
@ -1073,7 +1073,7 @@ defmodule Oban.Pro.Workflow do
|
|||
{__MODULE__, :on_flush, [[sup_workflow_id, workflow_id], conf]}
|
||||
|
||||
%{"workflow_id" => workflow_id} ->
|
||||
{__MODULE__, :on_flush, [workflow_id, conf]}
|
||||
{__MODULE__, :on_flush, [[workflow_id], conf]}
|
||||
|
||||
_ ->
|
||||
:ignore
|
||||
|
|
@ -1178,7 +1178,7 @@ defmodule Oban.Pro.Workflow do
|
|||
held_query = lock(Job, "FOR UPDATE SKIP LOCKED")
|
||||
|
||||
held_query =
|
||||
if Utils.has_legacy_suspended?(conf) do
|
||||
if Utils.has_legacy_workflows?(conf) do
|
||||
where(
|
||||
held_query,
|
||||
[j],
|
||||
|
|
@ -1950,32 +1950,12 @@ defmodule Oban.Pro.Workflow do
|
|||
_ -> graft_id
|
||||
end
|
||||
|
||||
sub_names = expand_sub_names(workflow)
|
||||
|
||||
changesets =
|
||||
Enum.map(workflow.changesets, fn changeset ->
|
||||
changeset = Changeset.update_change(changeset, :meta, &Map.put(&1, :workflow_id, root_id))
|
||||
|
||||
# Convert internal wildcard deps to explicit name deps so internal dependencies resolve
|
||||
# correctly.
|
||||
deps = get_in(changeset.changes, [:meta, :deps]) || []
|
||||
|
||||
updated_deps =
|
||||
Enum.flat_map(deps, fn
|
||||
[sub_id, "*"] ->
|
||||
case Map.get(sub_names, sub_id) do
|
||||
nil -> [[sub_id, "*"]]
|
||||
names -> names
|
||||
end
|
||||
|
||||
other ->
|
||||
[other]
|
||||
end)
|
||||
|
||||
if updated_deps != deps do
|
||||
Changeset.update_change(changeset, :meta, &Map.put(&1, :deps, updated_deps))
|
||||
else
|
||||
if get_in(changeset.changes, [:meta, :sup_workflow_id]) do
|
||||
changeset
|
||||
else
|
||||
Changeset.update_change(changeset, :meta, &Map.put(&1, :workflow_id, root_id))
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
@ -1986,21 +1966,6 @@ defmodule Oban.Pro.Workflow do
|
|||
|> add_workflow(graft_name, workflow, opts)
|
||||
end
|
||||
|
||||
defp expand_sub_names(workflow) do
|
||||
Enum.reduce(workflow.changesets, %{}, fn changeset, acc ->
|
||||
meta = changeset.changes[:meta] || %{}
|
||||
name = meta[:name]
|
||||
|
||||
sub_id = if meta[:sup_workflow_id], do: meta[:workflow_id]
|
||||
|
||||
if sub_id && name do
|
||||
Map.update(acc, sub_id, [name], &[name | &1])
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp extract_graft! do
|
||||
case Process.get(:oban_processing) do
|
||||
{_module, %{meta: %{"graft_id" => _}} = job, _opts} ->
|
||||
|
|
@ -2846,7 +2811,7 @@ defmodule Oban.Pro.Workflow do
|
|||
workflow_ids = Repo.all(conf, query, opts)
|
||||
|
||||
workflow_ids =
|
||||
if Utils.has_legacy_suspended?(conf) do
|
||||
if Utils.has_legacy_workflows?(conf) do
|
||||
legacy_query =
|
||||
Job
|
||||
|> select([j], fragment("?->>'workflow_id'", j.meta))
|
||||
|
|
@ -3053,9 +3018,9 @@ defmodule Oban.Pro.Workflow do
|
|||
now = DateTime.utc_now()
|
||||
|
||||
base =
|
||||
if Utils.has_legacy_suspended?(conf) do
|
||||
if Utils.has_legacy_workflows?(conf) do
|
||||
Job
|
||||
|> where([j], j.id in ^ids and j.state == "scheduled")
|
||||
|> where([j], j.id in ^ids and j.state in ["scheduled", "suspended"])
|
||||
|> update([j], set: [meta: drop_hold(j.meta)])
|
||||
else
|
||||
where(Job, [j], j.id in ^ids and j.state == "suspended")
|
||||
|
|
|
|||
11
vendor/oban_pro/mix.exs
vendored
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Oban.Pro.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@version "1.7.0"
|
||||
@version "1.7.5"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
|
@ -18,7 +18,7 @@ defmodule Oban.Pro.MixProject do
|
|||
package: package(),
|
||||
name: "Oban Pro",
|
||||
description: "Oban Pro Component",
|
||||
xref: [exclude: [Oban.JSON, Oban.Validation]],
|
||||
elixirc_options: [no_warn_undefined: [Oban.JSON, Oban.Validation]],
|
||||
|
||||
# Dialyzer
|
||||
dialyzer: [
|
||||
|
|
@ -65,6 +65,7 @@ defmodule Oban.Pro.MixProject do
|
|||
{:oban, "~> 2.21"},
|
||||
{:ecto_sql, "~> 3.12"},
|
||||
{:postgrex, "~> 0.22", optional: true},
|
||||
{:decimal, "~> 3.0", optional: true},
|
||||
|
||||
# Test and Dev
|
||||
{:stream_data, "~> 1.1", only: [:test, :dev]},
|
||||
|
|
@ -126,7 +127,11 @@ defmodule Oban.Pro.MixProject do
|
|||
logo: "assets/oban-pro-logo.svg",
|
||||
assets: %{"assets" => "assets"},
|
||||
nest_modules_by_prefix: nest_modules_by_prefix(),
|
||||
skip_undefined_reference_warnings_on: ["CHANGELOG.md", "guides/upgrading/v1.5.md"],
|
||||
skip_undefined_reference_warnings_on: [
|
||||
"CHANGELOG.md",
|
||||
"guides/upgrading/v1.5.md",
|
||||
"guides/upgrading/v1.7.md"
|
||||
],
|
||||
before_closing_body_tag: fn _ ->
|
||||
"""
|
||||
<script>document.querySelector('footer.footer p').remove()</script>
|
||||
|
|
|
|||
16
vendor/oban_pro/usage-rules/workers.md
vendored
|
|
@ -43,6 +43,22 @@ args_schema do
|
|||
end
|
||||
```
|
||||
|
||||
## Uniqueness
|
||||
|
||||
Configure uniqueness with state groups, not explicit state lists:
|
||||
|
||||
```elixir
|
||||
# Good — uses the :incomplete group
|
||||
use Oban.Pro.Worker, unique: [period: 60, states: :incomplete]
|
||||
|
||||
# Bad — explicit state lists drift out of sync with new states
|
||||
use Oban.Pro.Worker, unique: [period: 60, states: [:available, :scheduled, :executing]]
|
||||
```
|
||||
|
||||
Available groups: `:incomplete` (everything except `:cancelled`, `:completed`, `:discarded`) and
|
||||
`:scheduled` (just the `scheduled` state, safe for debouncing). Explicit lists silently miss states
|
||||
added in later releases and cause subtle duplication bugs.
|
||||
|
||||
## Encrypted jobs
|
||||
|
||||
Protect sensitive args with transparent encryption:
|
||||
|
|
|
|||
BIN
vendor/oban_web/.hex
vendored
114
vendor/oban_web/CHANGELOG.md
vendored
|
|
@ -15,7 +15,8 @@ workflow progress, activity counts, duration, and nested sub-workflows. Workflow
|
|||
by properties like name, workers, or status.
|
||||
|
||||
<video autoplay loop muted playsinline loading="lazy" preload="none" style="width: 100%; border-radius: 12px;">
|
||||
<source src="https://media.oban.pro/web-2-12-workflows-av1.mp4" type="video/mp4">
|
||||
<source src="https://media.oban.pro/web-2-12-workflows-av1.mp4" type="video/mp4" codecs="av01">
|
||||
<source src="https://media.oban.pro/web-2-12-workflows.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
Clicking into a workflow brings you to a detail view with an interactive graph showing jobs as
|
||||
|
|
@ -34,7 +35,8 @@ There's also a new page for viewing and managing cron entries. The table display
|
|||
dynamic entries with history sparklines and activity details.
|
||||
|
||||
<video autoplay loop muted playsinline loading="lazy" preload="none" style="width: 100%; border-radius: 12px;">
|
||||
<source src="https://media.oban.pro/web-2-12-crons-av1.mp4" type="video/mp4">
|
||||
<source src="https://media.oban.pro/web-2-12-crons-av1.mp4" type="video/mp4" codecs="av01">
|
||||
<source src="https://media.oban.pro/web-2-12-crons.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
The cron detail view includes natural language expressions like "Daily at 8:00 and 9:00" or
|
||||
|
|
@ -49,7 +51,8 @@ The job detail page is rebuilt with a full-width layout and a new timeline compo
|
|||
the job state machine as a branching diagram rather than a linear progression.
|
||||
|
||||
<video autoplay loop muted playsinline loading="lazy" preload="none" style="width: 100%; border-radius: 12px;">
|
||||
<source src="https://media.oban.pro/web-2-12-jobs-details-av1.mp4" type="video/mp4">
|
||||
<source src="https://media.oban.pro/web-2-12-jobs-details-av1.mp4" type="video/mp4" codecs="av01">
|
||||
<source src="https://media.oban.pro/web-2-12-jobs-details.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
A scoped chart displays execution history for that worker's previous jobs and jobs in an
|
||||
|
|
@ -66,13 +69,112 @@ throughput for each queue. The queue sidebar provided minimal value, and it was
|
|||
room for the additional data displayed per-row.
|
||||
|
||||
<video autoplay loop muted playsinline loading="lazy" preload="none" style="width: 100%; border-radius: 12px;">
|
||||
<source src="https://media.oban.pro/web-2-12-queues-av1.mp4" type="video/mp4">
|
||||
<source src="https://media.oban.pro/web-2-12-queues-av1.mp4" type="video/mp4" codecs="av01">
|
||||
<source src="https://media.oban.pro/web-2-12-queues.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
The queue detail page adds status badges for paused, partial, and terminating states, with
|
||||
pause/resume, stop, and edit buttons in the header. Partitioning controls are expanded with meta
|
||||
options and burst mode configuration.
|
||||
|
||||
## v2.12.5 - 2026-05-26
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Jobs] Display awaitable signals in the job details page
|
||||
|
||||
Add a section that decodes and displays signal payloads sent via
|
||||
`Oban.Pro.Worker.signal/2`. While a job is parked waiting, the section
|
||||
shows "Awaiting Signal" with the deadline. Once a signal arrives, it
|
||||
switches to "Received Signal".
|
||||
|
||||
- [Resolver] Add `format_signal/2` resolver callback
|
||||
|
||||
This allows customizing the decoded output,mirroring what's available
|
||||
with `format_recorded/2`.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [Jobs] Restrict unauthorized job editing and updates with new permission
|
||||
|
||||
The save-job event handler previously dispatched changes from any client
|
||||
without checking access controls, allowing a read-only user to rewrite a
|
||||
job's worker module and potentially trigger code execution on the next
|
||||
attempt. Editing now requires `:update_jobs` permission, which is
|
||||
enabled by default for `:all` and disabled for `:read_only`.
|
||||
|
||||
- [Cron] Prevent malicious cron expressions from unrestricted memory allocation
|
||||
|
||||
A maliciously crafted cron expression like "0 0 1--100000000 \* \*" could
|
||||
trigger multi-gigabyte allocations when `describe/1` eagerly expanded
|
||||
the range during formatting. Range, value, and step parsing now validate
|
||||
against per-field bounds and require ranges to be non-decreasing, so
|
||||
out-of-domain inputs are rejected before any expansion occurs.
|
||||
|
||||
## v2.12.4 - 2026-05-11
|
||||
|
||||
### Changes
|
||||
|
||||
- [Dashboard] Upgrade oban_pro dependency to full v1.7 release
|
||||
|
||||
Require the full v1.7 release rather than a release candidate.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [Dashboard] Escape names with reserved URL characters in paths
|
||||
|
||||
Safely handle queues or crons with names like `foo/bar.baz` when linking from the queues and
|
||||
crons tables.
|
||||
|
||||
- [Workflow] Fix workflow queries ignoring custom prefix
|
||||
|
||||
Two raw SQL fragments in WorkflowQuery referenced tables without a schema qualifier, causing a
|
||||
mismatch with the configured Oban prefix. Both fragments now inject the prefix as a quoted
|
||||
identifier so they honor the configured prefix like any other Oban.Repo queries.
|
||||
|
||||
- [Cron] Fix crash on cron page when an entry uses @reboot
|
||||
|
||||
The `next_at/2` function returns `:unknown` for `@reboot` crons, which fell through to
|
||||
`maybe_to_unix/1` and crashed. Guard the helper on a `DateTime` struct and return an empty
|
||||
string for anything else, safely convering `nil` or `:unknown`.
|
||||
|
||||
- [Cron] Fix crash loading cron history on SQLite
|
||||
|
||||
The `COALESCE` fragment used to compute the `finished_at` time was untyped, so Ecto couldn't
|
||||
apply the `:utc_datetime_usec` load callback. Postgrex would cast the value automatically, but
|
||||
exqlite returned a string and crashed downstream locations expecting a DateTime.
|
||||
|
||||
## v2.12.3 - 2026-04-15
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [Dashboard] Switch icons from inline SVG to CSS masks
|
||||
|
||||
CSP doesn't allow inline styles, which includes the url-masks that our app was using for icons.
|
||||
This removes the icons/asset pipeline in favor of the standard Tailwind plugin approach used by
|
||||
modern Phoenix apps
|
||||
|
||||
## v2.12.2 - 2026-03-31
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Jobs] Allow editing jobs in any state except `executing`
|
||||
|
||||
`Oban.update_job/3` allows editing any non-executing job, so the detail component's edit form
|
||||
should as well.
|
||||
|
||||
- [Standalone] Ship inetrc in standalone Docker image for native DNS resolution (#173)
|
||||
|
||||
The BEAM VM's built-in DNS resolver ignores `/etc/resolv.conf`, which prevents the standalone
|
||||
image from resolving internal hostnames on platforms like Fly.io and Kubernetes with CoreDNS.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [Query] Prevent table check with non-PostgreSQL engines
|
||||
|
||||
The crons and workflows table checks only apply to Pro, and only Postgres engines should check
|
||||
whether the tables exist.
|
||||
|
||||
## v2.12.1 - 2026-03-25
|
||||
|
||||
### Bug Fixes
|
||||
|
|
@ -164,7 +266,6 @@ features. For enhancements, a video is worth many thousands of words.
|
|||
- [Crons] Add cron parser for complex expressions
|
||||
|
||||
Parse cron fields into structured data before describing them, enabling support for:
|
||||
|
||||
- Combined DOM/DOW patterns like "The 1st, only on Mondays"
|
||||
- Complement detection, "Daily except the 1st" or "except Tuesdays"
|
||||
- Multiple hour values, "Daily at 8:00, 9:00, and 10:00"
|
||||
|
|
@ -172,7 +273,7 @@ features. For enhancements, a video is worth many thousands of words.
|
|||
|
||||
It's also switched to a 24-hour time format for international consistency.
|
||||
|
||||
- [Queues] Remove sidebar from queues page The sidebar filters (paused, terminating, modes, nodes) added little
|
||||
- [Queues] Remove sidebar from queues page The sidebar filters (paused, terminating, modes, nodes) added little
|
||||
|
||||
The sidebar filters (paused, terminating, modes, nodes) added little value for a typically small
|
||||
dataset while consuming significant screen space. Filtering remains available via the search
|
||||
|
|
@ -234,4 +335,3 @@ features. For enhancements, a video is worth many thousands of words.
|
|||
|
||||
The PG notifier can't (easily) connect to an external cluster for notifications. Connection is
|
||||
possible through the Postgres notifier.
|
||||
|
||||
|
|
|
|||
29
vendor/oban_web/lib/oban/web/assets.ex
vendored
|
|
@ -7,20 +7,6 @@ defmodule Oban.Web.Assets do
|
|||
|
||||
@static_path Application.app_dir(:oban_web, ["priv", "static"])
|
||||
|
||||
# Icons
|
||||
|
||||
icon_files =
|
||||
for dir <- ["outline", "solid", "special"],
|
||||
base = Path.join([@static_path, "icons", dir]),
|
||||
file <- File.ls!(base),
|
||||
String.ends_with?(file, ".svg") do
|
||||
path = Path.join(base, file)
|
||||
Module.put_attribute(__MODULE__, :external_resource, path)
|
||||
{Path.join(dir, file), File.read!(path)}
|
||||
end
|
||||
|
||||
@icons Map.new(icon_files)
|
||||
|
||||
# Font
|
||||
|
||||
@external_resource font_path = Path.join(@static_path, "fonts/Inter.woff2")
|
||||
|
|
@ -65,21 +51,6 @@ defmodule Oban.Web.Assets do
|
|||
serve_asset(conn, @font, "font/woff2")
|
||||
end
|
||||
|
||||
def call(conn, :icon) do
|
||||
icon_path = Enum.join(conn.path_params["path"], "/")
|
||||
|
||||
case Map.get(@icons, icon_path) do
|
||||
nil ->
|
||||
conn
|
||||
|> put_resp_header("content-type", "text/plain")
|
||||
|> send_resp(404, "Icon not found")
|
||||
|> halt()
|
||||
|
||||
contents ->
|
||||
serve_asset(conn, contents, "image/svg+xml")
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_asset(conn, contents, content_type) do
|
||||
conn
|
||||
|> put_resp_header("content-type", content_type)
|
||||
|
|
|
|||
54
vendor/oban_web/lib/oban/web/components/core.ex
vendored
|
|
@ -106,7 +106,7 @@ defmodule Oban.Web.Components.Core do
|
|||
phx-value-choice={@value}
|
||||
>
|
||||
<%= if to_string(@value) == to_string(@selected) do %>
|
||||
<Icons.check class="w-5 h-5" />
|
||||
<Icons.icon name="icon-check" class="w-5 h-5" />
|
||||
<% else %>
|
||||
<span class="block w-5 h-5"></span>
|
||||
<% end %>
|
||||
|
|
@ -142,7 +142,7 @@ defmodule Oban.Web.Components.Core do
|
|||
rel="check"
|
||||
>
|
||||
<div class={["w-4 h-4 flex items-center justify-center rounded border", @style]}>
|
||||
<Icons.check class="w-3 h-3 text-white dark:text-gray-900" />
|
||||
<Icons.icon name="icon-check" class="w-3 h-3 text-white dark:text-gray-900" />
|
||||
</div>
|
||||
</button>
|
||||
"""
|
||||
|
|
@ -174,9 +174,9 @@ defmodule Oban.Web.Components.Core do
|
|||
>
|
||||
<div class={["w-4 h-4 flex items-center justify-center rounded border", @style]}>
|
||||
<%= if @checked == :some do %>
|
||||
<Icons.indeterminate class="w-3 h-3 text-white dark:text-gray-900" />
|
||||
<Icons.icon name="icon-indeterminate" class="w-3 h-3 text-white dark:text-gray-900" />
|
||||
<% else %>
|
||||
<Icons.check class="w-3 h-3 text-white dark:text-gray-900" />
|
||||
<Icons.icon name="icon-check" class="w-3 h-3 text-white dark:text-gray-900" />
|
||||
<% end %>
|
||||
</div>
|
||||
</button>
|
||||
|
|
@ -203,40 +203,44 @@ defmodule Oban.Web.Components.Core do
|
|||
"""
|
||||
end
|
||||
|
||||
defp badge_icon(%{name: "camera"} = assigns), do: ~H[<Icons.camera class="h-4 w-4 shrink-0" />]
|
||||
defp badge_icon(%{name: "camera"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-camera" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "life_buoy"} = assigns),
|
||||
do: ~H[<Icons.life_buoy class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-life-buoy" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "lock_closed"} = assigns),
|
||||
do: ~H[<Icons.lock_closed class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-lock-closed" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "signal"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-signal" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "sparkles"} = assigns),
|
||||
do: ~H[<Icons.sparkles class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-sparkles" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "table_cells"} = assigns),
|
||||
do: ~H[<Icons.table_cells class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-table-cells" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "square_2x2"} = assigns),
|
||||
do: ~H[<Icons.square_2x2 class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-square-2x2" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "rectangle_group"} = assigns),
|
||||
do: ~H[<Icons.rectangle_group class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-rectangle-group" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "user_group"} = assigns),
|
||||
do: ~H[<Icons.user_group class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-user-group" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "link"} = assigns),
|
||||
do: ~H[<Icons.link class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-link" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "power"} = assigns),
|
||||
do: ~H[<Icons.power class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-power" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "pause_circle"} = assigns),
|
||||
do: ~H[<Icons.pause_circle class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-pause-circle" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "play_pause_circle"} = assigns),
|
||||
do: ~H[<Icons.play_pause_circle class="h-4 w-4 shrink-0" />]
|
||||
do: ~H[<Icons.icon name="icon-play-pause-circle" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
@doc """
|
||||
An icon-only button that expands to show label on hover. Supports disabled state.
|
||||
|
|
@ -309,17 +313,23 @@ defmodule Oban.Web.Components.Core do
|
|||
"""
|
||||
end
|
||||
|
||||
defp button_icon(%{name: "arrow_path"} = assigns), do: ~H[<Icons.arrow_path class={@class} />]
|
||||
defp button_icon(%{name: "arrow_path"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-arrow-path" class={@class} />]
|
||||
|
||||
defp button_icon(%{name: "pause_circle"} = assigns),
|
||||
do: ~H[<Icons.pause_circle class={@class} />]
|
||||
do: ~H[<Icons.icon name="icon-pause-circle" class={@class} />]
|
||||
|
||||
defp button_icon(%{name: "pencil_square"} = assigns),
|
||||
do: ~H[<Icons.pencil_square class={@class} />]
|
||||
do: ~H[<Icons.icon name="icon-pencil-square" class={@class} />]
|
||||
|
||||
defp button_icon(%{name: "play_circle"} = assigns), do: ~H[<Icons.play_circle class={@class} />]
|
||||
defp button_icon(%{name: "trash"} = assigns), do: ~H[<Icons.trash class={@class} />]
|
||||
defp button_icon(%{name: "x_circle"} = assigns), do: ~H[<Icons.x_circle class={@class} />]
|
||||
defp button_icon(%{name: "play_circle"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-play-circle" class={@class} />]
|
||||
|
||||
defp button_icon(%{name: "trash"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-trash" class={@class} />]
|
||||
|
||||
defp button_icon(%{name: "x_circle"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-x-circle" class={@class} />]
|
||||
|
||||
# Sparkline
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ defmodule Oban.Web.FormComponents do
|
|||
phx-hook="Tippy"
|
||||
class="translate-y-px"
|
||||
>
|
||||
<Icons.info_circle class="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
||||
<Icons.icon name="icon-info-circle" class="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
||||
</span>
|
||||
</label>
|
||||
<%= if @type == "textarea" do %>
|
||||
|
|
@ -110,7 +110,10 @@ defmodule Oban.Web.FormComponents do
|
|||
class="sr-only peer"
|
||||
/>
|
||||
<span class="block w-4 h-4 rounded border border-gray-300 dark:border-gray-600 peer-checked:border-blue-500 peer-checked:bg-blue-500" />
|
||||
<Icons.check class="w-3 h-3 text-white absolute top-0.5 left-0.5 hidden peer-checked:block" />
|
||||
<Icons.icon
|
||||
name="icon-check"
|
||||
class="w-3 h-3 text-white absolute top-0.5 left-0.5 hidden peer-checked:block"
|
||||
/>
|
||||
</div>
|
||||
{@label}
|
||||
<span
|
||||
|
|
@ -120,7 +123,7 @@ defmodule Oban.Web.FormComponents do
|
|||
phx-hook="Tippy"
|
||||
class="ml-1 translate-y-px"
|
||||
>
|
||||
<Icons.info_circle class="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
||||
<Icons.icon name="icon-info-circle" class="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
94
vendor/oban_web/lib/oban/web/components/icons.ex
vendored
|
|
@ -3,60 +3,17 @@ defmodule Oban.Web.Components.Icons do
|
|||
|
||||
use Phoenix.Component
|
||||
|
||||
@icons_path Application.app_dir(:oban_web, ["priv", "static", "icons"])
|
||||
attr :name, :string, required: true
|
||||
attr :class, :any, default: nil
|
||||
attr :rest, :global
|
||||
|
||||
@outline_path Path.join(@icons_path, "outline")
|
||||
@solid_path Path.join(@icons_path, "solid")
|
||||
|
||||
@outline_icons @outline_path
|
||||
|> File.ls!()
|
||||
|> Enum.filter(&String.ends_with?(&1, ".svg"))
|
||||
|> Enum.map(&String.trim_trailing(&1, ".svg"))
|
||||
|
||||
@solid_icons @solid_path
|
||||
|> File.ls!()
|
||||
|> Enum.filter(&String.ends_with?(&1, ".svg"))
|
||||
|> Enum.map(&String.trim_trailing(&1, ".svg"))
|
||||
|
||||
for icon <- @outline_icons do
|
||||
@external_resource Path.join(@outline_path, "#{icon}.svg")
|
||||
|
||||
func_name = icon |> String.replace("-", "_") |> String.to_atom()
|
||||
|
||||
@doc false
|
||||
attr :class, :any, default: "w-6 h-6"
|
||||
attr :rest, :global
|
||||
|
||||
def unquote(func_name)(assigns) do
|
||||
assigns = assign(assigns, :icon_path, "outline/#{unquote(icon)}.svg")
|
||||
|
||||
render_icon(assigns)
|
||||
end
|
||||
def icon(assigns) do
|
||||
~H"""
|
||||
<span class={[@name, @class]} aria-hidden="true" {@rest} />
|
||||
"""
|
||||
end
|
||||
|
||||
for icon <- @solid_icons do
|
||||
@external_resource Path.join(@solid_path, "#{icon}.svg")
|
||||
|
||||
func_name =
|
||||
icon
|
||||
|> String.replace("-", "_")
|
||||
|> Kernel.<>("_solid")
|
||||
|> String.to_atom()
|
||||
|
||||
@doc false
|
||||
attr :class, :any, default: "w-6 h-6"
|
||||
attr :rest, :global
|
||||
|
||||
def unquote(func_name)(assigns) do
|
||||
assigns = assign(assigns, :icon_path, "solid/#{unquote(icon)}.svg")
|
||||
|
||||
render_icon(assigns)
|
||||
end
|
||||
end
|
||||
|
||||
# Inline Icons
|
||||
|
||||
attr :rest, :global, default: %{class: "w-6 h-6"}
|
||||
attr :rest, :global, default: %{class: "size-6"}
|
||||
|
||||
def oban_pro_logo(assigns) do
|
||||
~H"""
|
||||
|
|
@ -116,7 +73,7 @@ defmodule Oban.Web.Components.Icons do
|
|||
"""
|
||||
end
|
||||
|
||||
attr :rest, :global, default: %{class: "w-6 h-6"}
|
||||
attr :rest, :global, default: %{class: "size-6"}
|
||||
|
||||
def spinner(assigns) do
|
||||
~H"""
|
||||
|
|
@ -126,37 +83,4 @@ defmodule Oban.Web.Components.Icons do
|
|||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_icon(assigns) do
|
||||
icon_url = icon_url(assigns.icon_path)
|
||||
class = normalize_class(assigns.class)
|
||||
assigns = assigns |> assign(:icon_url, icon_url) |> assign(:normalized_class, class)
|
||||
|
||||
~H"""
|
||||
<span
|
||||
class={["block bg-current", @normalized_class]}
|
||||
style={"mask-image: url(#{@icon_url}); -webkit-mask-image: url(#{@icon_url}); mask-size: contain; mask-repeat: no-repeat; mask-position: center;"}
|
||||
aria-hidden="true"
|
||||
{@rest}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
defp icon_url(path) do
|
||||
case Process.get(:routing) do
|
||||
{socket, prefix} ->
|
||||
Phoenix.VerifiedRoutes.unverified_path(
|
||||
socket,
|
||||
socket.router,
|
||||
"#{prefix}/icons/#{path}"
|
||||
)
|
||||
|
||||
_ ->
|
||||
"/icons/#{path}"
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_class(class) when is_binary(class), do: class
|
||||
defp normalize_class(class) when is_list(class), do: class
|
||||
defp normalize_class(nil), do: "w-6 h-6"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ defmodule Oban.Web.Layouts do
|
|||
<div class="p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 text-green-400">
|
||||
<Icons.check_circle />
|
||||
<Icons.icon name="icon-check-circle" />
|
||||
</div>
|
||||
<div class="ml-3 w-0 flex-1">
|
||||
<p class="text-sm leading-5 font-medium text-gray-900 dark:text-gray-100">
|
||||
|
|
@ -133,7 +133,7 @@ defmodule Oban.Web.Layouts do
|
|||
phx-click="lv:clear-flash"
|
||||
class="inline-flex text-gray-400 dark:text-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 focus:text-gray-500 transition ease-in-out duration-150"
|
||||
>
|
||||
<Icons.x_mark class="w-5 h-5" />
|
||||
<Icons.icon name="icon-x-mark" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ defmodule Oban.Web.SidebarComponents do
|
|||
phx-click={toggle(@name)}
|
||||
phx-hook="Tippy"
|
||||
>
|
||||
<Icons.chevron_right class="w-4 h-4 mt-1 mr-1 transition-transform rotate-90" />
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
class="w-4 h-4 mt-1 mr-1 transition-transform rotate-90"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<h3 class="dark:text-gray-200 font-bold">{String.capitalize(@name)}</h3>
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ defmodule Oban.Web.SortComponent do
|
|||
type="button"
|
||||
>
|
||||
<%= if @params.sort_dir == "asc" do %>
|
||||
<Icons.bars_arrow_down class="w-4 h-4" />
|
||||
<Icons.icon name="icon-bars-arrow-down" class="w-4 h-4" />
|
||||
<% else %>
|
||||
<Icons.bars_arrow_up class="w-4 h-4" />
|
||||
<Icons.icon name="icon-bars-arrow-up" class="w-4 h-4" />
|
||||
<% end %>
|
||||
<span class="ml-1 block capitalize">
|
||||
{String.replace(@params.sort_by, "_", " ")}
|
||||
|
|
@ -71,7 +71,7 @@ defmodule Oban.Web.SortComponent do
|
|||
role="option"
|
||||
>
|
||||
<%= if @value == @selected do %>
|
||||
<Icons.check class="w-4 h-4 text-blue-500" />
|
||||
<Icons.icon name="icon-check" class="w-4 h-4 text-blue-500" />
|
||||
<% else %>
|
||||
<span class="block w-4 h-4"></span>
|
||||
<% end %>
|
||||
|
|
|
|||
39
vendor/oban_web/lib/oban/web/cron_expr.ex
vendored
|
|
@ -45,10 +45,10 @@ defmodule Oban.Web.CronExpr do
|
|||
|
||||
defp describe_parsed(expression) do
|
||||
with [min, hrs, dom, "*", dow] <- String.split(expression, " ", parts: 5),
|
||||
{:ok, parsed_min} <- parse_field(min),
|
||||
{:ok, parsed_hrs} <- parse_field(hrs),
|
||||
{:ok, parsed_dom} <- parse_field(dom),
|
||||
{:ok, parsed_dow} <- parse_field(dow, @days_of_week_translations) do
|
||||
{:ok, parsed_min} <- parse_field(min, 0..59),
|
||||
{:ok, parsed_hrs} <- parse_field(hrs, 0..23),
|
||||
{:ok, parsed_dom} <- parse_field(dom, 1..31),
|
||||
{:ok, parsed_dow} <- parse_field(dow, 0..7, @days_of_week_translations) do
|
||||
combine_description(parsed_min, parsed_hrs, parsed_dom, parsed_dow)
|
||||
else
|
||||
_ -> nil
|
||||
|
|
@ -57,11 +57,11 @@ defmodule Oban.Web.CronExpr do
|
|||
|
||||
# Field Parsing
|
||||
|
||||
defp parse_field(field, translations \\ %{}) do
|
||||
defp parse_field(field, bounds, translations \\ %{}) do
|
||||
parts =
|
||||
field
|
||||
|> String.split(",")
|
||||
|> Enum.map(&parse_part(&1, translations))
|
||||
|> Enum.map(&parse_part(&1, bounds, translations))
|
||||
|
||||
if Enum.any?(parts, &(&1 == :error)) do
|
||||
:error
|
||||
|
|
@ -70,37 +70,40 @@ defmodule Oban.Web.CronExpr do
|
|||
end
|
||||
end
|
||||
|
||||
defp parse_part("*", _translations), do: :wildcard
|
||||
defp parse_part("*", _bounds, _translations), do: :wildcard
|
||||
|
||||
defp parse_part("*/" <> step, _translations) do
|
||||
defp parse_part("*/" <> step, bounds, _translations) do
|
||||
case Integer.parse(step) do
|
||||
{num, ""} when num > 0 -> {:step, num}
|
||||
{num, ""} when num > bounds.first and num <= bounds.last -> {:step, num}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_part(part, translations) do
|
||||
defp parse_part(part, bounds, translations) do
|
||||
if String.contains?(part, "-") do
|
||||
parse_range(part, translations)
|
||||
parse_range(part, bounds, translations)
|
||||
else
|
||||
parse_value(part, translations)
|
||||
parse_value(part, bounds, translations)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_range(part, translations) do
|
||||
defp parse_range(part, bounds, translations) do
|
||||
with [start_str, end_str] <- String.split(part, "-", parts: 2),
|
||||
{:ok, start_val} <- translate_or_parse(start_str, translations),
|
||||
{:ok, end_val} <- translate_or_parse(end_str, translations) do
|
||||
{:ok, end_val} <- translate_or_parse(end_str, translations),
|
||||
true <- start_val in bounds and end_val in bounds and end_val >= start_val do
|
||||
{:range, start_val, end_val}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_value(part, translations) do
|
||||
case translate_or_parse(part, translations) do
|
||||
{:ok, val} -> {:value, val}
|
||||
:error -> :error
|
||||
defp parse_value(part, bounds, translations) do
|
||||
with {:ok, val} <- translate_or_parse(part, translations),
|
||||
true <- val in bounds do
|
||||
{:value, val}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ defmodule Oban.Web.DashboardLive do
|
|||
page = resolve_page(params)
|
||||
|
||||
Process.put(:routing, {socket, prefix})
|
||||
Process.put(:csp_nonce_style, csp_nonces.style)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|
|
|
|||
6
vendor/oban_web/lib/oban/web/helpers.ex
vendored
|
|
@ -28,7 +28,7 @@ defmodule Oban.Web.Helpers do
|
|||
|
||||
def oban_path(route, params) when is_list(route) do
|
||||
route
|
||||
|> Enum.join("/")
|
||||
|> Enum.map_join("/", &encode_segment/1)
|
||||
|> oban_path(params)
|
||||
end
|
||||
|
||||
|
|
@ -50,6 +50,10 @@ defmodule Oban.Web.Helpers do
|
|||
end
|
||||
end
|
||||
|
||||
defp encode_segment(segment) do
|
||||
segment |> to_string() |> URI.encode(&URI.char_unreserved?/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Toggle filterable params into a patch path.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -47,10 +47,15 @@ defmodule Oban.Web.ConnectivityComponent do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<div id="connectivity" data-title={@title} phx-hook="Tippy">
|
||||
<Icons.bolt_circle :if={@status == :solitary} class="w-6 h-6 text-yellow-500" />
|
||||
<Icons.bolt_slash :if={@status == :isolated} class="w-6 h-6 animate-pulse text-red-500" />
|
||||
<Icons.exclamation_circle
|
||||
<Icons.icon :if={@status == :solitary} name="icon-bolt-circle" class="w-6 h-6 text-yellow-500" />
|
||||
<Icons.icon
|
||||
:if={@status == :isolated}
|
||||
name="icon-bolt-slash"
|
||||
class="w-6 h-6 animate-pulse text-red-500"
|
||||
/>
|
||||
<Icons.icon
|
||||
:if={@status == :disconnected}
|
||||
name="icon-exclamation-circle"
|
||||
class="w-6 h-6 animate-pulse text-red-500"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ defmodule Oban.Web.Crons.DetailComponent do
|
|||
data-title="Back to crons"
|
||||
phx-hook="Tippy"
|
||||
>
|
||||
<Icons.arrow_left class="w-5 h-5" />
|
||||
<Icons.icon name="icon-arrow-left" class="w-5 h-5" />
|
||||
<span class="text-lg font-bold ml-2">
|
||||
{@cron.worker}
|
||||
<span :if={show_name?(@cron)} class="font-normal text-gray-500 dark:text-gray-400">
|
||||
|
|
@ -140,7 +140,7 @@ defmodule Oban.Web.Crons.DetailComponent do
|
|||
|
||||
<div class="px-3 py-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<h3 class="flex font-semibold mb-3 space-x-2 text-gray-400">
|
||||
<Icons.pencil_square />
|
||||
<Icons.icon name="icon-pencil-square" />
|
||||
<span>Edit Configuration</span>
|
||||
</h3>
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ defmodule Oban.Web.Crons.DetailComponent do
|
|||
class="text-xs text-gray-500 dark:text-gray-400 hover:underline"
|
||||
>
|
||||
Editing requires DynamicCron
|
||||
<Icons.arrow_top_right_on_square class="w-3 h-3 inline-block" />
|
||||
<Icons.icon name="icon-arrow-top-right-on-square" class="w-3 h-3 inline-block" />
|
||||
</a>
|
||||
<.save_button disabled={not @changed?} />
|
||||
</div>
|
||||
|
|
@ -257,7 +257,7 @@ defmodule Oban.Web.Crons.DetailComponent do
|
|||
navigate={oban_path(:jobs, %{meta: [["cron_name"], @cron.name], state: "completed"})}
|
||||
class="absolute right-4 top-4 flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-blue-900 dark:hover:text-blue-300 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
View all jobs <Icons.arrow_right class="w-3 h-3" />
|
||||
View all jobs <Icons.icon name="icon-arrow-right" class="w-3 h-3" />
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ defmodule Oban.Web.Crons.Helpers do
|
|||
|
||||
def state_icon(%{paused: true} = assigns) do
|
||||
~H"""
|
||||
<Icons.pause_circle class="w-5 h-5 text-gray-400" />
|
||||
<Icons.icon name="icon-pause-circle" class="w-5 h-5 text-gray-400" />
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
@ -21,21 +21,21 @@ defmodule Oban.Web.Crons.Helpers do
|
|||
~H"""
|
||||
<%= case @state do %>
|
||||
<% "available" -> %>
|
||||
<Icons.ellipsis_horizontal_circle class={["w-5 h-5", @color_class]} />
|
||||
<Icons.icon name="icon-ellipsis-horizontal-circle" class={["w-5 h-5", @color_class]} />
|
||||
<% "cancelled" -> %>
|
||||
<Icons.x_circle class={["w-5 h-5", @color_class]} />
|
||||
<Icons.icon name="icon-x-circle" class={["w-5 h-5", @color_class]} />
|
||||
<% "completed" -> %>
|
||||
<Icons.check_circle class={["w-5 h-5", @color_class]} />
|
||||
<Icons.icon name="icon-check-circle" class={["w-5 h-5", @color_class]} />
|
||||
<% "discarded" -> %>
|
||||
<Icons.exclamation_circle class={["w-5 h-5", @color_class]} />
|
||||
<Icons.icon name="icon-exclamation-circle" class={["w-5 h-5", @color_class]} />
|
||||
<% "executing" -> %>
|
||||
<Icons.play_circle class={["w-5 h-5", @color_class]} />
|
||||
<Icons.icon name="icon-play-circle" class={["w-5 h-5", @color_class]} />
|
||||
<% "retryable" -> %>
|
||||
<Icons.arrow_path class={["w-5 h-5", @color_class]} />
|
||||
<Icons.icon name="icon-arrow-path" class={["w-5 h-5", @color_class]} />
|
||||
<% "scheduled" -> %>
|
||||
<Icons.clock class={["w-5 h-5", @color_class]} />
|
||||
<Icons.icon name="icon-clock" class={["w-5 h-5", @color_class]} />
|
||||
<% _ -> %>
|
||||
<Icons.minus_circle class="w-5 h-5 text-gray-400" />
|
||||
<Icons.icon name="icon-minus-circle" class="w-5 h-5 text-gray-400" />
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
|
@ -43,14 +43,14 @@ defmodule Oban.Web.Crons.Helpers do
|
|||
@doc """
|
||||
Converts a NaiveDateTime to unix milliseconds, returning empty string for nil.
|
||||
"""
|
||||
def maybe_to_unix(nil), do: ""
|
||||
|
||||
def maybe_to_unix(timestamp) do
|
||||
def maybe_to_unix(timestamp) when is_struct(timestamp) do
|
||||
timestamp
|
||||
|> DateTime.from_naive!("Etc/UTC")
|
||||
|> DateTime.to_unix(:millisecond)
|
||||
end
|
||||
|
||||
def maybe_to_unix(_timestamp), do: ""
|
||||
|
||||
def show_name?(%{dynamic?: true, name: name, worker: worker}), do: name != worker
|
||||
def show_name?(_cron), do: false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ defmodule Oban.Web.Crons.NewComponent do
|
|||
phx-target={@myself}
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icons.x_mark class="h-6 w-6" />
|
||||
<Icons.icon name="icon-x-mark" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ defmodule Oban.Web.Crons.TableComponent do
|
|||
</ul>
|
||||
|
||||
<div :if={Enum.empty?(@crontab)} class="py-16 px-6 text-center">
|
||||
<Icons.clock class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<Icons.icon name="icon-clock" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<h3 class="mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100">No crons</h3>
|
||||
<p class="mt-2 text-base text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||
Crons run jobs on a schedule. Configure them in your Oban supervisor or create them dynamically.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Oban.Web.HelpComponent do
|
|||
phx-click={JS.toggle(to: "#help-menu")}
|
||||
type="button"
|
||||
>
|
||||
<Icons.question_mark_circle />
|
||||
<Icons.icon name="icon-question-mark-circle" />
|
||||
</button>
|
||||
|
||||
<ul
|
||||
|
|
@ -35,7 +35,7 @@ defmodule Oban.Web.HelpComponent do
|
|||
rel="noopener noreferrer"
|
||||
class="flex items-center space-x-2 w-full"
|
||||
>
|
||||
<Icons.arrow_top_right_on_square class="w-5 h-5" />
|
||||
<Icons.icon name="icon-arrow-top-right-on-square" class="w-5 h-5" />
|
||||
<span class="text-gray-800 dark:text-gray-200">Documentation</span>
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -45,7 +45,7 @@ defmodule Oban.Web.HelpComponent do
|
|||
phx-click={JS.hide(to: "#help-menu") |> JS.exec("data-shortcut", to: "#shortcuts")}
|
||||
role="option"
|
||||
>
|
||||
<Icons.command_line class="w-5 h-5" />
|
||||
<Icons.icon name="icon-command-line" class="w-5 h-5" />
|
||||
<span class="text-gray-800 dark:text-gray-200">Keyboard shortcuts</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ defmodule Oban.Web.InstancesComponent do
|
|||
phx-value-name={name}
|
||||
>
|
||||
<%= if name == @active do %>
|
||||
<Icons.check class="w-4 h-4 text-blue-500" />
|
||||
<Icons.icon name="icon-check" class="w-4 h-4 text-blue-500" />
|
||||
<% else %>
|
||||
<span class="block w-4 h-4"></span>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -56,10 +56,13 @@ defmodule Oban.Web.Jobs.ChartComponent do
|
|||
phx-click={toggle_chart(@myself)}
|
||||
phx-hook="Tippy"
|
||||
>
|
||||
<Icons.chevron_right class={[
|
||||
"w-5 h-5 mr-2 transition-transform",
|
||||
if(@visible, do: "rotate-90")
|
||||
]} />
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
class={[
|
||||
"w-5 h-5 mr-2 transition-transform",
|
||||
if(@visible, do: "rotate-90")
|
||||
]}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<h3 class="text-base font-semibold">
|
||||
|
|
@ -89,7 +92,7 @@ defmodule Oban.Web.Jobs.ChartComponent do
|
|||
target={@myself}
|
||||
title="Change metric series"
|
||||
>
|
||||
<Icons.chart_bar_square />
|
||||
<Icons.icon name="icon-chart-bar-square" />
|
||||
</Core.dropdown_button>
|
||||
|
||||
<Core.dropdown_button
|
||||
|
|
@ -100,7 +103,7 @@ defmodule Oban.Web.Jobs.ChartComponent do
|
|||
target={@myself}
|
||||
title="Change slice period"
|
||||
>
|
||||
<Icons.clock />
|
||||
<Icons.icon name="icon-clock" />
|
||||
</Core.dropdown_button>
|
||||
|
||||
<Core.dropdown_button
|
||||
|
|
@ -111,7 +114,7 @@ defmodule Oban.Web.Jobs.ChartComponent do
|
|||
target={@myself}
|
||||
title="Change metric grouping"
|
||||
>
|
||||
<Icons.rectangle_group />
|
||||
<Icons.icon name="icon-rectangle-group" />
|
||||
</Core.dropdown_button>
|
||||
|
||||
<Core.dropdown_button
|
||||
|
|
@ -122,7 +125,7 @@ defmodule Oban.Web.Jobs.ChartComponent do
|
|||
target={@myself}
|
||||
title="Change percentile"
|
||||
>
|
||||
<Icons.percent_square />
|
||||
<Icons.icon name="icon-percent-square" />
|
||||
</Core.dropdown_button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
phx-hook="HistoryBack"
|
||||
type="button"
|
||||
>
|
||||
<Icons.arrow_left class="w-5 h-5" />
|
||||
<Icons.icon name="icon-arrow-left" class="w-5 h-5" />
|
||||
<span class="text-lg font-bold ml-2">{job_title(@job)}</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -64,6 +64,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
<Core.status_badge :if={@job.meta["chunk"]} icon="user_group" label="Chunk" />
|
||||
<Core.status_badge :if={@job.meta["chain"]} icon="link" label="Chain" />
|
||||
<Core.status_badge :if={@job.meta["recorded"]} icon="camera" label="Recorded" />
|
||||
<Core.status_badge :if={signal_status(@job) != :none} icon="signal" label="Signal" />
|
||||
<Core.status_badge :if={@job.meta["encrypted"]} icon="lock_closed" label="Encrypted" />
|
||||
<Core.status_badge :if={@job.meta["structured"]} icon="table_cells" label="Structured" />
|
||||
<Core.status_badge :if={@job.meta["decorated"]} icon="sparkles" label="Decorated" />
|
||||
|
|
@ -109,7 +110,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
label="Edit"
|
||||
color="violet"
|
||||
tooltip="Edit this job"
|
||||
disabled={not editable?(@job)}
|
||||
disabled={executing?(@job)}
|
||||
phx-click={scroll_to_edit()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -223,7 +224,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
Workflow
|
||||
</span>
|
||||
<span class="text-base text-gray-800 dark:text-gray-200 flex items-center">
|
||||
<Icons.rectangle_group class="w-4 h-4 mr-1.5 text-violet-500" />
|
||||
<Icons.icon name="icon-rectangle-group" class="w-4 h-4 mr-1.5 text-violet-500" />
|
||||
<span class="truncate">{workflow_display_name(@job)}</span>
|
||||
</span>
|
||||
</.link>
|
||||
|
|
@ -241,7 +242,8 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
phx-click="toggle-diagnostics"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<Icons.chevron_right
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
id="diagnostics-chevron"
|
||||
class={["w-5 h-5 transition-transform", if(@diagnostics_open?, do: "rotate-90")]}
|
||||
/>
|
||||
|
|
@ -324,7 +326,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
phx-hook="Tippy"
|
||||
phx-click={copy_to_clipboard(@diagnostics["info"]["current_stacktrace"])}
|
||||
>
|
||||
<Icons.clipboard class="w-4 h-4" />
|
||||
<Icons.icon name="icon-clipboard" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<%= if @diagnostics["info"]["current_stacktrace"] do %>
|
||||
|
|
@ -345,7 +347,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center space-x-2 px-2 text-gray-400 dark:text-gray-500">
|
||||
<Icons.clock class="w-5 h-5" />
|
||||
<Icons.icon name="icon-clock" class="w-5 h-5" />
|
||||
<span class="text-sm">
|
||||
<%= if executing?(@job) do %>
|
||||
Waiting for diagnostics...
|
||||
|
|
@ -374,7 +376,8 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
class="flex items-center w-full space-x-2 px-2 py-1.5 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
phx-click={toggle_errors()}
|
||||
>
|
||||
<Icons.chevron_right
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
id="errors-chevron"
|
||||
class={["w-5 h-5 transition-transform", if(Enum.any?(@job.errors), do: "rotate-90")]}
|
||||
/>
|
||||
|
|
@ -440,7 +443,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
)
|
||||
]}
|
||||
>
|
||||
<Icons.chevron_left class="w-5 h-5" />
|
||||
<Icons.icon name="icon-chevron-left" class="w-5 h-5" />
|
||||
</button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 tabular min-w-[4rem] text-center">
|
||||
{@error_index + 1} of {length(@job.errors)}
|
||||
|
|
@ -460,7 +463,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
)
|
||||
]}
|
||||
>
|
||||
<Icons.chevron_right class="w-5 h-5" />
|
||||
<Icons.icon name="icon-chevron-right" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -468,7 +471,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
<.error_entry errors={@job.errors} index={@error_index} sort={@error_sort} />
|
||||
<% else %>
|
||||
<div class="flex items-center space-x-2 px-2 text-gray-400 dark:text-gray-500">
|
||||
<Icons.check_circle class="w-5 h-5" />
|
||||
<Icons.icon name="icon-check-circle" class="w-5 h-5" />
|
||||
<span class="text-sm">No errors recorded</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -482,24 +485,25 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
class="flex items-center w-full space-x-2 px-2 py-1.5 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
phx-click={toggle_edit()}
|
||||
>
|
||||
<Icons.chevron_right
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
id="edit-chevron"
|
||||
class={["w-5 h-5 transition-transform", if(editable?(@job), do: "rotate-90")]}
|
||||
class={["w-5 h-5 transition-transform", if(not executing?(@job), do: "rotate-90")]}
|
||||
/>
|
||||
<span class="font-semibold">Edit Job</span>
|
||||
<span
|
||||
:if={not editable?(@job)}
|
||||
:if={executing?(@job)}
|
||||
id="edit-hint"
|
||||
class="flex items-center"
|
||||
data-title="Only available, scheduled, and retryable jobs can be edited"
|
||||
data-title="Executing jobs can't be edited"
|
||||
phx-hook="Tippy"
|
||||
>
|
||||
<Icons.info_circle class="w-4 h-4 text-gray-400" />
|
||||
<Icons.icon name="icon-info-circle" class="w-4 h-4 text-gray-400" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div id="edit-content" class={["mt-3", unless(editable?(@job), do: "hidden")]}>
|
||||
<fieldset disabled={not editable?(@job)}>
|
||||
<div id="edit-content" class={["mt-3", if(executing?(@job), do: "hidden")]}>
|
||||
<fieldset disabled={executing?(@job) or not can?(:update_jobs, @access)}>
|
||||
<form
|
||||
id="job-edit-form"
|
||||
class="grid grid-cols-4 gap-4 bg-gray-50 dark:bg-gray-800 rounded-md p-4"
|
||||
|
|
@ -567,7 +571,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={not @edit_changed?}
|
||||
disabled={not @edit_changed? or not can?(:update_jobs, @access)}
|
||||
class="px-6 py-2 bg-blue-500 text-white text-sm font-medium rounded-md hover:bg-blue-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Save Changes
|
||||
|
|
@ -626,7 +630,11 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
class="flex items-center w-full space-x-2 px-2 py-1.5 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
phx-click={toggle_job_data()}
|
||||
>
|
||||
<Icons.chevron_right id="job-data-chevron" class="w-5 h-5 transition-transform rotate-90" />
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
id="job-data-chevron"
|
||||
class="w-5 h-5 transition-transform rotate-90"
|
||||
/>
|
||||
<span class="font-semibold">Job Data</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -645,7 +653,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
phx-hook="Tippy"
|
||||
phx-click={copy_to_clipboard(format_args(@job, @resolver))}
|
||||
>
|
||||
<Icons.clipboard class="w-4 h-4" />
|
||||
<Icons.icon name="icon-clipboard" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<pre class="font-mono text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap break-all">{format_args(@job, @resolver)}</pre>
|
||||
|
|
@ -664,7 +672,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
phx-hook="Tippy"
|
||||
phx-click={copy_to_clipboard(format_meta(@job, @resolver))}
|
||||
>
|
||||
<Icons.clipboard class="w-4 h-4" />
|
||||
<Icons.icon name="icon-clipboard" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<pre class="font-mono text-sm text-gray-500 dark:text-gray-400 whitespace-pre-wrap break-all">{format_meta(@job, @resolver)}</pre>
|
||||
|
|
@ -688,12 +696,39 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
phx-hook="Tippy"
|
||||
phx-click={copy_to_clipboard(format_recorded(@job, @resolver))}
|
||||
>
|
||||
<Icons.clipboard class="w-4 h-4" />
|
||||
<Icons.icon name="icon-clipboard" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<pre class="font-mono text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap break-all">{format_recorded(@job, @resolver)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :if={signal_status(@job) != :none} class="mt-4">
|
||||
<div class="relative bg-gray-50 dark:bg-gray-800 rounded-md p-4">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<h4 class="font-medium text-xs uppercase text-gray-500 dark:text-gray-400">
|
||||
{signal_heading(@job)}
|
||||
</h4>
|
||||
<.pro_badge id="signal-pro-badge" tooltip="Awaitable signal from Oban.Pro.Worker" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="copy-signal"
|
||||
class={[
|
||||
"w-9 h-9 -mr-2 -mt-2 flex items-center justify-center rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-white dark:hover:bg-gray-700 cursor-pointer",
|
||||
signal_status(@job) != :received && "invisible"
|
||||
]}
|
||||
data-title="Copy to clipboard"
|
||||
phx-hook="Tippy"
|
||||
phx-click={copy_to_clipboard(format_signal(@job, @resolver))}
|
||||
>
|
||||
<Icons.icon name="icon-clipboard" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<pre class="font-mono text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap break-all">{format_signal(@job, @resolver)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
@ -778,14 +813,16 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
def handle_event("save-job", params, socket) do
|
||||
job = socket.assigns.job
|
||||
|
||||
changes =
|
||||
params
|
||||
|> parse_edit_params(job)
|
||||
|> Enum.reject(fn {_key, val} -> is_nil(val) end)
|
||||
|> Map.new()
|
||||
if can?(:update_jobs, socket.assigns.access) do
|
||||
changes =
|
||||
params
|
||||
|> parse_edit_params(job)
|
||||
|> Enum.reject(fn {_key, val} -> is_nil(val) end)
|
||||
|> Map.new()
|
||||
|
||||
if map_size(changes) > 0 do
|
||||
send(self(), {:update_job, job, changes})
|
||||
if map_size(changes) > 0 do
|
||||
send(self(), {:update_job, job, changes})
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, edit_changed?: false)}
|
||||
|
|
@ -798,12 +835,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
end
|
||||
|
||||
defp format_meta(%{meta: meta} = job, resolver) do
|
||||
job =
|
||||
if meta["recorded"] do
|
||||
%{job | meta: Map.delete(meta, "return")}
|
||||
else
|
||||
job
|
||||
end
|
||||
job = %{job | meta: Map.drop(meta, ["return", "signal"])}
|
||||
|
||||
Resolver.call_with_fallback(resolver, :format_job_meta, [job])
|
||||
end
|
||||
|
|
@ -821,6 +853,41 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
end
|
||||
end
|
||||
|
||||
defp format_signal(%{meta: %{"signal" => value}} = job, resolver) do
|
||||
Resolver.call_with_fallback(resolver, :format_signal, [value, job])
|
||||
end
|
||||
|
||||
defp format_signal(%{meta: %{"wait_until" => wait_until}}, _resolver) do
|
||||
case wait_until_to_datetime(wait_until) do
|
||||
{:ok, datetime} -> "Deadline #{Timing.datetime_to_words(datetime)}"
|
||||
:infinity -> "No deadline"
|
||||
:error -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp signal_status(%{meta: %{"signal" => _}}), do: :received
|
||||
defp signal_status(%{meta: %{"wait_until" => _}}), do: :awaiting
|
||||
defp signal_status(_), do: :none
|
||||
|
||||
defp signal_heading(job) do
|
||||
case signal_status(job) do
|
||||
:received -> "Received Signal"
|
||||
:awaiting -> "Awaiting Signal"
|
||||
:none -> "Signal"
|
||||
end
|
||||
end
|
||||
|
||||
defp wait_until_to_datetime("infinity"), do: :infinity
|
||||
|
||||
defp wait_until_to_datetime(ms) when is_integer(ms) do
|
||||
case DateTime.from_unix(ms, :millisecond) do
|
||||
{:ok, datetime} -> {:ok, datetime |> DateTime.to_naive() |> NaiveDateTime.truncate(:second)}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp wait_until_to_datetime(_), do: :error
|
||||
|
||||
defp error_entry(assigns) do
|
||||
error =
|
||||
assigns.errors
|
||||
|
|
@ -919,10 +986,6 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
JS.dispatch("phx:copy-to-clipboard", detail: %{text: text})
|
||||
end
|
||||
|
||||
defp editable?(%{state: state}) do
|
||||
state in ~w(scheduled retryable available)
|
||||
end
|
||||
|
||||
defp executing?(%{state: state}), do: state == "executing"
|
||||
|
||||
defp format_bytes(nil), do: "—"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ defmodule Oban.Web.Jobs.HistoryChartComponent do
|
|||
class="flex items-center w-full space-x-2 px-2 py-1.5 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
phx-click={toggle_section()}
|
||||
>
|
||||
<Icons.chevron_right id="history-chevron" class="w-5 h-5 transition-transform rotate-90" />
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
id="history-chevron"
|
||||
class="w-5 h-5 transition-transform rotate-90"
|
||||
/>
|
||||
<span class="font-semibold">Recent Executions</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -28,7 +32,7 @@ defmodule Oban.Web.Jobs.HistoryChartComponent do
|
|||
navigate={all_jobs_path(@job)}
|
||||
class="absolute right-4 top-4 flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-blue-900 dark:hover:text-blue-300 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
View all jobs <Icons.arrow_right class="w-3 h-3" />
|
||||
View all jobs <Icons.icon name="icon-arrow-right" class="w-3 h-3" />
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ defmodule Oban.Web.Jobs.NewComponent do
|
|||
phx-target={@myself}
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icons.x_mark class="h-6 w-6" />
|
||||
<Icons.icon name="icon-x-mark" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ defmodule Oban.Web.Jobs.TableComponent do
|
|||
|
||||
<div :if={Enum.empty?(@jobs)} class="text-lg text-center py-12">
|
||||
<div class="flex items-center justify-center space-x-2 text-gray-600 dark:text-gray-300">
|
||||
<Icons.no_symbol /> <span>No jobs match the current set of filters.</span>
|
||||
<Icons.icon name="icon-no-symbol" /> <span>No jobs match the current set of filters.</span>
|
||||
</div>
|
||||
<p :if={is_integer(@query_limit)} class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Filtering limited to latest {integer_to_delimited(@query_limit)} jobs. See <a
|
||||
|
|
@ -111,16 +111,18 @@ defmodule Oban.Web.Jobs.TableComponent do
|
|||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center space-x-1">
|
||||
<Icons.life_buoy
|
||||
<Icons.icon
|
||||
:if={Map.has_key?(@job.meta, "rescued")}
|
||||
name="icon-life-buoy"
|
||||
class="h-5 w-5 text-gray-500 dark:text-gray-300"
|
||||
id={"job-rescued-#{assigns.job.id}"}
|
||||
phx-hook="Tippy"
|
||||
data-title="Rescued by lifeline"
|
||||
/>
|
||||
|
||||
<Icons.crossbones_circle_solid
|
||||
<Icons.icon
|
||||
:if={orphaned?(@job, @producers)}
|
||||
name="icon-crossbones-circle-solid"
|
||||
class="h-5 w-5 text-gray-500 dark:text-gray-300"
|
||||
id={"job-orphaned-#{assigns.job.id}"}
|
||||
phx-hook="Tippy"
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ defmodule Oban.Web.Jobs.TimelineComponent do
|
|||
|
||||
defp state_icon(%{icon: :pending} = assigns) do
|
||||
~H"""
|
||||
<Icons.ellipsis_horizontal_circle class="w-5 h-5" />
|
||||
<Icons.icon name="icon-ellipsis-horizontal-circle" class="w-5 h-5" />
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ defmodule Oban.Web.Jobs.TimelineComponent do
|
|||
|
||||
defp state_icon(%{icon: :done} = assigns) do
|
||||
~H"""
|
||||
<Icons.check_circle class="w-5 h-5" />
|
||||
<Icons.icon name="icon-check-circle" class="w-5 h-5" />
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
phx-hook="HistoryBack"
|
||||
type="button"
|
||||
>
|
||||
<Icons.arrow_left class="w-5 h-5" />
|
||||
<Icons.icon name="icon-arrow-left" class="w-5 h-5" />
|
||||
<span class="text-lg capitalize font-bold ml-2">{@queue} Queue</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
navigate={oban_path(:jobs, %{queues: @queue, state: "completed"})}
|
||||
class="absolute right-4 top-4 flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-blue-900 dark:hover:text-blue-300 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
View all jobs <Icons.arrow_right class="w-3 h-3" />
|
||||
View all jobs <Icons.icon name="icon-arrow-right" class="w-3 h-3" />
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
|
|
@ -339,7 +339,8 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
class="flex items-center w-full space-x-2 px-2 py-1.5 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
phx-click={toggle_instances(@myself)}
|
||||
>
|
||||
<Icons.chevron_right
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
id="instances-chevron"
|
||||
class={["w-5 h-5 transition-transform", if(@instances_open?, do: "rotate-90")]}
|
||||
/>
|
||||
|
|
@ -421,7 +422,8 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
class="flex items-center w-full space-x-2 px-2 py-1.5 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
phx-click={toggle_config(@myself)}
|
||||
>
|
||||
<Icons.chevron_right
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
id="config-chevron"
|
||||
class={["w-5 h-5 transition-transform", if(@config_open?, do: "rotate-90")]}
|
||||
/>
|
||||
|
|
@ -437,7 +439,7 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
phx-submit="local-submit"
|
||||
>
|
||||
<h3 class="flex items-center mb-4">
|
||||
<Icons.map_pin class="w-5 h-5 mr-1 text-gray-500" />
|
||||
<Icons.icon name="icon-map-pin" class="w-5 h-5 mr-1 text-gray-500" />
|
||||
<span class="text-base font-medium">Local Limit</span>
|
||||
</h3>
|
||||
|
||||
|
|
@ -467,7 +469,7 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="flex items-center">
|
||||
<Icons.globe class="w-5 h-5 mr-1 text-gray-500" />
|
||||
<Icons.icon name="icon-globe" class="w-5 h-5 mr-1 text-gray-500" />
|
||||
<span class="text-base font-medium">Global Limit</span>
|
||||
<span
|
||||
id="global-limit-info"
|
||||
|
|
@ -475,7 +477,10 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
phx-hook="Tippy"
|
||||
class="ml-1"
|
||||
>
|
||||
<Icons.info_circle class="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
||||
<Icons.icon
|
||||
name="icon-info-circle"
|
||||
class="w-4 h-4 text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
|
|
@ -565,7 +570,7 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
class="text-xs text-gray-500 dark:text-gray-400 hover:underline"
|
||||
>
|
||||
Requires Smart Engine
|
||||
<Icons.arrow_top_right_on_square class="w-3 h-3 inline-block" />
|
||||
<Icons.icon name="icon-arrow-top-right-on-square" class="w-3 h-3 inline-block" />
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -579,7 +584,7 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="flex items-center">
|
||||
<Icons.arrow_trending_down class="w-5 h-5 mr-1 text-gray-500" />
|
||||
<Icons.icon name="icon-arrow-trending-down" class="w-5 h-5 mr-1 text-gray-500" />
|
||||
<span class="text-base font-medium">Rate Limit</span>
|
||||
<span
|
||||
id="rate-limit-info"
|
||||
|
|
@ -587,7 +592,10 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
phx-hook="Tippy"
|
||||
class="ml-1"
|
||||
>
|
||||
<Icons.info_circle class="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
||||
<Icons.icon
|
||||
name="icon-info-circle"
|
||||
class="w-4 h-4 text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
|
|
@ -677,7 +685,7 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
class="text-xs text-gray-500 dark:text-gray-400 hover:underline"
|
||||
>
|
||||
Requires Smart Engine
|
||||
<Icons.arrow_top_right_on_square class="w-3 h-3 inline-block" />
|
||||
<Icons.icon name="icon-arrow-top-right-on-square" class="w-3 h-3 inline-block" />
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -913,13 +921,13 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
class={"#{if @enabled, do: "opacity-0 ease-out duration-100", else: "opacity-100 ease-in duration-200"} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icons.x_mark class="h-3 w-3 text-gray-400" />
|
||||
<Icons.icon name="icon-x-mark" class="h-3 w-3 text-gray-400" />
|
||||
</span>
|
||||
<span
|
||||
class={"#{if @enabled, do: "opacity-100 ease-in duration-200", else: "opacity-0 ease-out duration-100"} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icons.check class="h-3 w-3 text-blue-500" />
|
||||
<Icons.icon name="icon-check" class="h-3 w-3 text-blue-500" />
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -930,7 +938,7 @@ defmodule Oban.Web.Queues.DetailComponent do
|
|||
~H"""
|
||||
<div class="flex items-center justify-end mt-4 space-x-2">
|
||||
<%= if @locked do %>
|
||||
<Icons.lock_closed class="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<Icons.icon name="icon-lock-closed" class="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<% end %>
|
||||
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -115,13 +115,13 @@ defmodule Oban.Web.Queues.DetailInstanceComponent do
|
|||
:if={@terminating?}
|
||||
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300"
|
||||
>
|
||||
<Icons.power class="w-3 h-3 mr-0.5" /> Stopping
|
||||
<Icons.icon name="icon-power" class="w-3 h-3 mr-0.5" /> Stopping
|
||||
</span>
|
||||
<span
|
||||
:if={@paused and not @terminating?}
|
||||
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300"
|
||||
>
|
||||
<Icons.pause_circle class="w-3 h-3 mr-0.5" /> Paused
|
||||
<Icons.icon name="icon-pause-circle" class="w-3 h-3 mr-0.5" /> Paused
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
|
@ -147,9 +147,9 @@ defmodule Oban.Web.Queues.DetailInstanceComponent do
|
|||
type="button"
|
||||
>
|
||||
<%= if @paused do %>
|
||||
<Icons.play_circle class="w-5 h-5" />
|
||||
<Icons.icon name="icon-play-circle" class="w-5 h-5" />
|
||||
<% else %>
|
||||
<Icons.pause_circle class="w-5 h-5" />
|
||||
<Icons.icon name="icon-pause-circle" class="w-5 h-5" />
|
||||
<% end %>
|
||||
</button>
|
||||
"""
|
||||
|
|
@ -168,7 +168,7 @@ defmodule Oban.Web.Queues.DetailInstanceComponent do
|
|||
phx-target={@target}
|
||||
type="button"
|
||||
>
|
||||
<Icons.pencil_square class="w-5 h-5" />
|
||||
<Icons.icon name="icon-pencil-square" class="w-5 h-5" />
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ defmodule Oban.Web.Queues.TableComponent do
|
|||
</ul>
|
||||
|
||||
<div :if={Enum.empty?(@queues) and Enum.empty?(@checks)} class="py-16 px-6 text-center">
|
||||
<Icons.queue_list class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<Icons.icon name="icon-queue-list" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<h3 class="mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100">No queues</h3>
|
||||
<p class="mt-2 text-base text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||
Queues process jobs concurrently. They'll appear here once your Oban instance starts with queues configured.
|
||||
|
|
@ -44,7 +44,7 @@ defmodule Oban.Web.Queues.TableComponent do
|
|||
:if={Enum.empty?(@queues) and not Enum.empty?(@checks)}
|
||||
class="flex items-center justify-center py-12 space-x-2 text-lg text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<Icons.no_symbol /> <span>No queues match the current filters.</span>
|
||||
<Icons.icon name="icon-no-symbol" /> <span>No queues match the current filters.</span>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
|
|
@ -115,22 +115,25 @@ defmodule Oban.Web.Queues.TableComponent do
|
|||
<span class="w-14 text-left tabular pl-2">{exec}/{limit}</span>
|
||||
</span>
|
||||
<div class="w-14 flex items-center justify-start space-x-1 text-gray-400 dark:text-gray-500">
|
||||
<Icons.globe
|
||||
<Icons.icon
|
||||
:if={Queue.global_limit?(@queue)}
|
||||
name="icon-globe"
|
||||
class="w-4 h-4"
|
||||
data-title="Global limit"
|
||||
id={"#{@queue.name}-has-global"}
|
||||
phx-hook="Tippy"
|
||||
/>
|
||||
<Icons.arrow_trending_down
|
||||
<Icons.icon
|
||||
:if={Queue.rate_limit?(@queue)}
|
||||
name="icon-arrow-trending-down"
|
||||
class="w-4 h-4"
|
||||
data-title="Rate limit"
|
||||
id={"#{@queue.name}-has-rate"}
|
||||
phx-hook="Tippy"
|
||||
/>
|
||||
<Icons.view_columns
|
||||
<Icons.icon
|
||||
:if={Queue.partitioned?(@queue)}
|
||||
name="icon-view-columns"
|
||||
class="w-4 h-4"
|
||||
data-title="Partitioned"
|
||||
id={"#{@queue.name}-has-partition"}
|
||||
|
|
@ -203,24 +206,27 @@ defmodule Oban.Web.Queues.TableComponent do
|
|||
</span>
|
||||
|
||||
<div class="w-20 pr-3 flex justify-center items-center space-x-1">
|
||||
<Icons.pause_circle
|
||||
<Icons.icon
|
||||
:if={Queue.all_paused?(@queue)}
|
||||
name="icon-pause-circle"
|
||||
class="w-5 h-5"
|
||||
data-title="All paused"
|
||||
id={"#{@queue.name}-is-paused"}
|
||||
phx-hook="Tippy"
|
||||
rel="is-paused"
|
||||
/>
|
||||
<Icons.play_pause_circle
|
||||
<Icons.icon
|
||||
:if={Queue.any_paused?(@queue) and not Queue.all_paused?(@queue)}
|
||||
name="icon-play-pause-circle"
|
||||
class="w-5 h-5"
|
||||
data-title="Some paused"
|
||||
id={"#{@queue.name}-is-some-paused"}
|
||||
phx-hook="Tippy"
|
||||
rel="has-some-paused"
|
||||
/>
|
||||
<Icons.power
|
||||
<Icons.icon
|
||||
:if={Queue.terminating?(@queue)}
|
||||
name="icon-power"
|
||||
class="w-5 h-5"
|
||||
data-title="Terminating"
|
||||
id={"#{@queue.name}-is-terminating"}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ defmodule Oban.Web.RefreshComponent do
|
|||
phx-click={JS.toggle(to: "#refresh-menu")}
|
||||
type="button"
|
||||
>
|
||||
<Icons.arrow_path_rounded />
|
||||
<Icons.icon name="icon-arrow-path-rounded" />
|
||||
<span class="ml-1 leading-6 text-sm">{:proplists.get_value(@refresh, @options)}</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ defmodule Oban.Web.RefreshComponent do
|
|||
phx-value-interval={@value}
|
||||
>
|
||||
<%= if @value == @refresh do %>
|
||||
<Icons.check class="w-5 h-5" />
|
||||
<Icons.icon name="icon-check" class="w-5 h-5" />
|
||||
<% else %>
|
||||
<span class="block w-5 h-5"></span>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ defmodule Oban.Web.SearchComponent do
|
|||
<%= if @loading do %>
|
||||
<Icons.spinner class="w-5 h-5 text-gray-200 animate-spin dark:text-gray-600 fill-violet-500" />
|
||||
<% else %>
|
||||
<Icons.magnifying_glass class="w-5 h-5 text-gray-500" />
|
||||
<Icons.icon name="icon-magnifying-glass" class="w-5 h-5 text-gray-500" />
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ defmodule Oban.Web.SearchComponent do
|
|||
phx-click="clear"
|
||||
type="reset"
|
||||
>
|
||||
<Icons.x_circle class="w-5 h-5" />
|
||||
<Icons.icon name="icon-x-circle" class="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<nav
|
||||
|
|
@ -102,7 +102,7 @@ defmodule Oban.Web.SearchComponent do
|
|||
/>
|
||||
|
||||
<div :if={Enum.empty?(@suggestions)} class="w-full flex items-center space-x-2 p-1">
|
||||
<Icons.exclamation_circle class="w-5 h-5 text-gray-400" />
|
||||
<Icons.icon name="icon-exclamation-circle" class="w-5 h-5 text-gray-400" />
|
||||
<span class="text-gray-700">No suggestions matching <b>"{@buffer}"</b></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -113,7 +113,7 @@ defmodule Oban.Web.SearchComponent do
|
|||
title="Read the filtering and searching guide"
|
||||
target="_blank"
|
||||
>
|
||||
<Icons.info_circle class="w-4 h-4" />
|
||||
<Icons.icon name="icon-info-circle" class="w-4 h-4" />
|
||||
<span class="text-xs">Filtering Tips</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
|
@ -139,7 +139,7 @@ defmodule Oban.Web.SearchComponent do
|
|||
phx-value-terms={@terms}
|
||||
phx-target="#search"
|
||||
>
|
||||
<Icons.x_mark class="w-5 h-5" />
|
||||
<Icons.icon name="icon-x-mark" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ defmodule Oban.Web.ShortcutsComponent do
|
|||
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
||||
aria-label="close"
|
||||
>
|
||||
<Icons.x_mark />
|
||||
<Icons.icon name="icon-x-mark" />
|
||||
</button>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -71,11 +71,11 @@ defmodule Oban.Web.ThemeComponent do
|
|||
~H"""
|
||||
<%= case @theme do %>
|
||||
<% "light" -> %>
|
||||
<Icons.sun />
|
||||
<Icons.icon name="icon-sun" />
|
||||
<% "dark" -> %>
|
||||
<Icons.moon />
|
||||
<Icons.icon name="icon-moon" />
|
||||
<% "system" -> %>
|
||||
<Icons.computer_desktop />
|
||||
<Icons.icon name="icon-computer-desktop" />
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
|
|
|||
|
|
@ -64,7 +64,10 @@ defmodule Oban.Web.Workflows.DetailComponent do
|
|||
<% else %>
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div class="text-center">
|
||||
<Icons.rectangle_group class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<Icons.icon
|
||||
name="icon-rectangle-group"
|
||||
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
<h3 class="mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
Workflow not found
|
||||
</h3>
|
||||
|
|
@ -97,7 +100,7 @@ defmodule Oban.Web.Workflows.DetailComponent do
|
|||
phx-hook="HistoryBack"
|
||||
type="button"
|
||||
>
|
||||
<Icons.arrow_left class="w-5 h-5" />
|
||||
<Icons.icon name="icon-arrow-left" class="w-5 h-5" />
|
||||
<span class="text-lg font-bold ml-2">{@workflow.name || @workflow.id}</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -142,7 +145,7 @@ defmodule Oban.Web.Workflows.DetailComponent do
|
|||
defp parent_breadcrumb(assigns) do
|
||||
~H"""
|
||||
<div class="flex items-center ml-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
<Icons.arrow_turn_down_right class="w-4 h-4 mr-1" />
|
||||
<Icons.icon name="icon-arrow-turn-down-right" class="w-4 h-4 mr-1" />
|
||||
<span>sub-workflow of</span>
|
||||
<.link
|
||||
navigate={oban_path([:workflows, @parent.id])}
|
||||
|
|
@ -233,10 +236,13 @@ defmodule Oban.Web.Workflows.DetailComponent do
|
|||
phx-click="toggle-graph"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<Icons.chevron_right class={[
|
||||
"w-5 h-5 transition-transform",
|
||||
@graph_open? && "rotate-90"
|
||||
]} />
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
class={[
|
||||
"w-5 h-5 transition-transform",
|
||||
@graph_open? && "rotate-90"
|
||||
]}
|
||||
/>
|
||||
<span class="font-semibold">Workflow Graph</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -419,10 +425,13 @@ defmodule Oban.Web.Workflows.DetailComponent do
|
|||
phx-click="toggle-subs"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<Icons.chevron_right class={[
|
||||
"w-5 h-5 transition-transform",
|
||||
@subs_open? && "rotate-90"
|
||||
]} />
|
||||
<Icons.icon
|
||||
name="icon-chevron-right"
|
||||
class={[
|
||||
"w-5 h-5 transition-transform",
|
||||
@subs_open? && "rotate-90"
|
||||
]}
|
||||
/>
|
||||
<span class="font-semibold">
|
||||
Sub-workflows
|
||||
<span class="text-gray-400 font-normal">
|
||||
|
|
@ -522,17 +531,17 @@ defmodule Oban.Web.Workflows.DetailComponent do
|
|||
<span class="inline-flex">
|
||||
<%= case @state do %>
|
||||
<% "executing" -> %>
|
||||
<Icons.play_circle class="w-5 h-5 text-emerald-400" />
|
||||
<Icons.icon name="icon-play-circle" class="w-5 h-5 text-emerald-400" />
|
||||
<% "completed" -> %>
|
||||
<Icons.check_circle class="w-5 h-5 text-cyan-400" />
|
||||
<Icons.icon name="icon-check-circle" class="w-5 h-5 text-cyan-400" />
|
||||
<% "retryable" -> %>
|
||||
<Icons.arrow_path class="w-5 h-5 text-yellow-400" />
|
||||
<Icons.icon name="icon-arrow-path" class="w-5 h-5 text-yellow-400" />
|
||||
<% "cancelled" -> %>
|
||||
<Icons.x_circle class="w-5 h-5 text-violet-400" />
|
||||
<Icons.icon name="icon-x-circle" class="w-5 h-5 text-violet-400" />
|
||||
<% "discarded" -> %>
|
||||
<Icons.exclamation_circle class="w-5 h-5 text-rose-400" />
|
||||
<Icons.icon name="icon-exclamation-circle" class="w-5 h-5 text-rose-400" />
|
||||
<% _ -> %>
|
||||
<Icons.minus_circle class="w-5 h-5 text-gray-400" />
|
||||
<Icons.icon name="icon-minus-circle" class="w-5 h-5 text-gray-400" />
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ defmodule Oban.Web.Workflows.TableComponent do
|
|||
</ul>
|
||||
|
||||
<div :if={Enum.empty?(@workflows)} class="py-16 px-6 text-center">
|
||||
<Icons.rectangle_group class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<Icons.icon
|
||||
name="icon-rectangle-group"
|
||||
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
<h3 class="mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100">No workflows</h3>
|
||||
<p class="mt-2 text-base text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||
Workflows coordinate jobs with dependencies. They'll appear here once jobs with workflow metadata are enqueued.
|
||||
|
|
@ -249,15 +252,15 @@ defmodule Oban.Web.Workflows.TableComponent do
|
|||
<span data-title={status_title(@state)} id={"workflow-state-#{@id}"} phx-hook="Tippy">
|
||||
<%= case @state do %>
|
||||
<% :executing -> %>
|
||||
<Icons.play_circle class="w-5 h-5 text-emerald-400" />
|
||||
<Icons.icon name="icon-play-circle" class="w-5 h-5 text-emerald-400" />
|
||||
<% :completed -> %>
|
||||
<Icons.check_circle class="w-5 h-5 text-cyan-400" />
|
||||
<Icons.icon name="icon-check-circle" class="w-5 h-5 text-cyan-400" />
|
||||
<% :cancelled -> %>
|
||||
<Icons.x_circle class="w-5 h-5 text-violet-400" />
|
||||
<Icons.icon name="icon-x-circle" class="w-5 h-5 text-violet-400" />
|
||||
<% :discarded -> %>
|
||||
<Icons.exclamation_circle class="w-5 h-5 text-rose-400" />
|
||||
<Icons.icon name="icon-exclamation-circle" class="w-5 h-5 text-rose-400" />
|
||||
<% _ -> %>
|
||||
<Icons.minus_circle class="w-5 h-5 text-gray-400" />
|
||||
<Icons.icon name="icon-minus-circle" class="w-5 h-5 text-gray-400" />
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ defmodule Oban.Web.CronsPage do
|
|||
"text-gray-400 dark:text-gray-500 border-gray-200 dark:border-gray-800 cursor-not-allowed opacity-50"
|
||||
]}
|
||||
>
|
||||
<Icons.plus_circle class="mr-1 h-4 w-4" /> New
|
||||
<Icons.icon name="icon-plus-circle" class="mr-1 h-4 w-4" /> New
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
10
vendor/oban_web/lib/oban/web/pages/jobs_page.ex
vendored
|
|
@ -89,7 +89,7 @@ defmodule Oban.Web.JobsPage do
|
|||
click="cancel-jobs"
|
||||
target={@myself}
|
||||
>
|
||||
<:icon><Icons.x_circle class="w-5 h-5" /></:icon>
|
||||
<:icon><Icons.icon name="icon-x-circle" class="w-5 h-5" /></:icon>
|
||||
<:title>Cancel Jobs</:title>
|
||||
</Core.action_button>
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ defmodule Oban.Web.JobsPage do
|
|||
click="retry-jobs"
|
||||
target={@myself}
|
||||
>
|
||||
<:icon><Icons.arrow_right_circle class="w-5 h-5" /></:icon>
|
||||
<:icon><Icons.icon name="icon-arrow-right-circle" class="w-5 h-5" /></:icon>
|
||||
<:title>Retry Jobs</:title>
|
||||
</Core.action_button>
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ defmodule Oban.Web.JobsPage do
|
|||
click="retry-jobs"
|
||||
target={@myself}
|
||||
>
|
||||
<:icon><Icons.arrow_right_circle class="w-5 h-5" /></:icon>
|
||||
<:icon><Icons.icon name="icon-arrow-right-circle" class="w-5 h-5" /></:icon>
|
||||
<:title>Run Jobs Now</:title>
|
||||
</Core.action_button>
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ defmodule Oban.Web.JobsPage do
|
|||
target={@myself}
|
||||
danger={true}
|
||||
>
|
||||
<:icon><Icons.trash class="w-5 h-5" /></:icon>
|
||||
<:icon><Icons.icon name="icon-trash" class="w-5 h-5" /></:icon>
|
||||
<:title>Delete Jobs</:title>
|
||||
</Core.action_button>
|
||||
</div>
|
||||
|
|
@ -162,7 +162,7 @@ defmodule Oban.Web.JobsPage do
|
|||
"text-gray-400 dark:text-gray-500 border-gray-200 dark:border-gray-800 cursor-not-allowed opacity-50"
|
||||
]}
|
||||
>
|
||||
<Icons.plus_circle class="mr-1 h-4 w-4" /> New
|
||||
<Icons.icon name="icon-plus-circle" class="mr-1 h-4 w-4" /> New
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ defmodule Oban.Web.QueuesPage do
|
|||
click="pause-queues"
|
||||
target={@myself}
|
||||
>
|
||||
<:icon><Icons.pause_circle class="w-5 h-5" /></:icon>
|
||||
<:icon><Icons.icon name="icon-pause-circle" class="w-5 h-5" /></:icon>
|
||||
<:title>Pause Queues</:title>
|
||||
</Core.action_button>
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ defmodule Oban.Web.QueuesPage do
|
|||
click="resume-queues"
|
||||
target={@myself}
|
||||
>
|
||||
<:icon><Icons.play_circle class="w-5 h-5" /></:icon>
|
||||
<:icon><Icons.icon name="icon-play-circle" class="w-5 h-5" /></:icon>
|
||||
<:title>Resume Queues</:title>
|
||||
</Core.action_button>
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ defmodule Oban.Web.QueuesPage do
|
|||
target={@myself}
|
||||
danger={true}
|
||||
>
|
||||
<:icon><Icons.x_circle class="w-5 h-5" /></:icon>
|
||||
<:icon><Icons.icon name="icon-x-circle" class="w-5 h-5" /></:icon>
|
||||
<:title>Stop Queues</:title>
|
||||
</Core.action_button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ defmodule Oban.Web.WorkflowsPage do
|
|||
~H"""
|
||||
<div class="flex flex-col items-center justify-center py-16 px-6">
|
||||
<div class="flex items-center justify-center w-16 h-16 rounded-full bg-violet-100 dark:bg-violet-900/30 mb-6">
|
||||
<Icons.rectangle_group class="w-8 h-8 text-violet-500 dark:text-violet-400" />
|
||||
<Icons.icon name="icon-rectangle-group" class="w-8 h-8 text-violet-500 dark:text-violet-400" />
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-3">
|
||||
|
|
@ -136,21 +136,21 @@ defmodule Oban.Web.WorkflowsPage do
|
|||
|
||||
<ul class="text-left text-gray-600 dark:text-gray-400 space-y-3 mb-8">
|
||||
<li class="flex items-start">
|
||||
<Icons.check class="w-5 h-5 text-violet-500 mr-2 mt-0.5 shrink-0" />
|
||||
<Icons.icon name="icon-check" class="w-5 h-5 text-violet-500 mr-2 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">Fully Distributed</span>
|
||||
— high availability and scalability across your infrastructure
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<Icons.check class="w-5 h-5 text-violet-500 mr-2 mt-0.5 shrink-0" />
|
||||
<Icons.icon name="icon-check" class="w-5 h-5 text-violet-500 mr-2 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">Cascading Context</span>
|
||||
— pass cumulative context between jobs for seamless data flow
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<Icons.check class="w-5 h-5 text-violet-500 mr-2 mt-0.5 shrink-0" />
|
||||
<Icons.icon name="icon-check" class="w-5 h-5 text-violet-500 mr-2 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">Nested Sub-Workflows</span>
|
||||
— compose hierarchically for better organization and reusability
|
||||
|
|
@ -163,7 +163,7 @@ defmodule Oban.Web.WorkflowsPage do
|
|||
target="_blank"
|
||||
class="inline-flex items-center px-5 py-2.5 rounded-lg bg-violet-600 hover:bg-violet-700 text-white font-medium transition-colors"
|
||||
>
|
||||
Learn about Oban Pro <Icons.arrow_top_right_on_square class="w-4 h-4 ml-2" />
|
||||
Learn about Oban Pro <Icons.icon name="icon-arrow-top-right-on-square" class="w-4 h-4 ml-2" />
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
|
|
@ -211,7 +211,7 @@ defmodule Oban.Web.WorkflowsPage do
|
|||
)
|
||||
|
||||
true ->
|
||||
workflows = WorkflowQuery.all_workflows(params, conf)
|
||||
workflows = WorkflowQuery.all_workflows(conf, params)
|
||||
limit = params.limit
|
||||
|
||||
assign(socket,
|
||||
|
|
|
|||
|
|
@ -195,7 +195,11 @@ defmodule Oban.Web.CronQuery do
|
|||
state: j.state,
|
||||
scheduled_at: j.scheduled_at,
|
||||
attempted_at: j.attempted_at,
|
||||
finished_at: fragment("COALESCE(?, ?, ?)", j.completed_at, j.cancelled_at, j.discarded_at)
|
||||
finished_at:
|
||||
type(
|
||||
fragment("COALESCE(?, ?, ?)", j.completed_at, j.cancelled_at, j.discarded_at),
|
||||
:utc_datetime_usec
|
||||
)
|
||||
})
|
||||
|
||||
conf
|
||||
|
|
@ -228,6 +232,8 @@ defmodule Oban.Web.CronQuery do
|
|||
query = select(Oban.Pro.Cron, [c], {c.expression, c.worker, c.opts, c.name, true, c.paused})
|
||||
|
||||
Repo.all(conf, query)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -278,7 +284,10 @@ defmodule Oban.Web.CronQuery do
|
|||
attempted_at: o.attempted_at,
|
||||
scheduled_at: o.scheduled_at,
|
||||
finished_at:
|
||||
fragment("COALESCE(?, ?, ?)", o.completed_at, o.cancelled_at, o.discarded_at)
|
||||
type(
|
||||
fragment("COALESCE(?, ?, ?)", o.completed_at, o.cancelled_at, o.discarded_at),
|
||||
:utc_datetime_usec
|
||||
)
|
||||
},
|
||||
select_merge: %{
|
||||
rn: over(row_number(), partition_by: o.meta["cron_name"], order_by: [desc: o.id])
|
||||
|
|
|
|||
|
|
@ -126,30 +126,32 @@ defmodule Oban.Web.WorkflowQuery do
|
|||
end
|
||||
end
|
||||
|
||||
defmacrop sub_workflow_parent_dep(sub_workflow_id, sup_workflow_id) do
|
||||
defmacrop sub_workflow_parent_dep(prefix, sub_workflow_id, sup_workflow_id) do
|
||||
quote do
|
||||
fragment(
|
||||
"""
|
||||
(SELECT dep->>1
|
||||
FROM oban_jobs j2,
|
||||
FROM ?.oban_jobs j2,
|
||||
LATERAL jsonb_array_elements(j2.meta->'deps') AS dep
|
||||
WHERE j2.meta->>'workflow_id' = ?
|
||||
AND jsonb_typeof(dep) = 'array'
|
||||
AND dep->>0 = ?
|
||||
LIMIT 1)
|
||||
""",
|
||||
identifier(^unquote(prefix)),
|
||||
unquote(sub_workflow_id),
|
||||
unquote(sup_workflow_id)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop sub_workflow_states(parent_id) do
|
||||
defmacrop sub_workflow_states(prefix, parent_id) do
|
||||
quote do
|
||||
fragment(
|
||||
"""
|
||||
(SELECT COALESCE(array_agg(state), '{}') FROM oban_workflows WHERE parent_id = ?)
|
||||
(SELECT COALESCE(array_agg(state), '{}') FROM ?.oban_workflows WHERE parent_id = ?)
|
||||
""",
|
||||
identifier(^unquote(prefix)),
|
||||
unquote(parent_id)
|
||||
)
|
||||
end
|
||||
|
|
@ -279,18 +281,19 @@ defmodule Oban.Web.WorkflowQuery do
|
|||
|
||||
# Querying
|
||||
|
||||
def all_workflows(params, conf) do
|
||||
def all_workflows(conf, params) do
|
||||
limit = Map.get(params, :limit, 10)
|
||||
sort_by = Map.get(params, :sort_by, "inserted")
|
||||
sort_dir = Map.get(params, :sort_dir, "desc")
|
||||
dir = String.to_existing_atom(sort_dir)
|
||||
prefix = conf.prefix
|
||||
|
||||
Workflow
|
||||
|> where([wf], is_nil(wf.parent_id))
|
||||
|> apply_filters(params)
|
||||
|> apply_sort(sort_by, dir)
|
||||
|> limit(^limit)
|
||||
|> select([wf], {wf, sub_workflow_states(wf.id)})
|
||||
|> select([wf], {wf, sub_workflow_states(prefix, wf.id)})
|
||||
|> then(&Repo.all(conf, &1))
|
||||
|> Enum.map(&build_workflow/1)
|
||||
end
|
||||
|
|
@ -446,6 +449,8 @@ defmodule Oban.Web.WorkflowQuery do
|
|||
end
|
||||
|
||||
defp workflow_graph_subs(conf, workflow_id) do
|
||||
prefix = conf.prefix
|
||||
|
||||
base_query =
|
||||
Job
|
||||
|> where([j], has_sup_workflow_id(j.meta, ^workflow_id))
|
||||
|
|
@ -464,7 +469,7 @@ defmodule Oban.Web.WorkflowQuery do
|
|||
workflow_name: s.workflow_name,
|
||||
sub_name: s.sub_name,
|
||||
state: s.state,
|
||||
parent_dep: sub_workflow_parent_dep(s.workflow_id, ^workflow_id)
|
||||
parent_dep: sub_workflow_parent_dep(prefix, s.workflow_id, ^workflow_id)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
55
vendor/oban_web/lib/oban/web/resolver.ex
vendored
|
|
@ -41,6 +41,13 @@ defmodule Oban.Web.Resolver do
|
|||
|> inspect(charlists: :as_lists, pretty: true)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def format_signal(signal, _job) do
|
||||
signal
|
||||
|> Oban.Web.Resolver.decode_signal()
|
||||
|> inspect(charlists: :as_lists, pretty: true)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def jobs_query_limit(_state), do: 100_000
|
||||
|
||||
|
|
@ -138,6 +145,7 @@ defmodule Oban.Web.Resolver do
|
|||
| :scale_queues
|
||||
| :stop_queues
|
||||
| :update_crons
|
||||
| :update_jobs
|
||||
|
||||
@type qualifier :: :args | :meta | :nodes | :queues | :tags | :workers
|
||||
|
||||
|
|
@ -234,6 +242,35 @@ defmodule Oban.Web.Resolver do
|
|||
"""
|
||||
@callback format_recorded(recorded :: term(), job :: Job.t()) :: iodata()
|
||||
|
||||
@doc """
|
||||
Customize the formatting of awaitable signal payloads wherever they are displayed.
|
||||
|
||||
This callback is similar to `c:format_recorded/2`, but it accepts both the signal binary and
|
||||
the job to help augment the output.
|
||||
|
||||
Note that you **must decode the signal binary** prior to inspecting it.
|
||||
|
||||
## Examples
|
||||
|
||||
Disable pretty printing and change the output width to 98 characters:
|
||||
|
||||
def format_signal(signal, _job) do
|
||||
signal
|
||||
|> Oban.Web.Resolver.decode_signal()
|
||||
|> inspect(pretty: false, width: 98)
|
||||
end
|
||||
|
||||
Decode the signal value without the `:safe` flag set, to allow decoding terms with unknown
|
||||
atoms:
|
||||
|
||||
def format_signal(signal, _job) do
|
||||
signal
|
||||
|> Oban.Web.Resolver.decode_signal([])
|
||||
|> inspect(pretty: false, width: 98)
|
||||
end
|
||||
"""
|
||||
@callback format_signal(signal :: term(), job :: Job.t()) :: iodata()
|
||||
|
||||
@doc """
|
||||
Extract the current user from a `Plug.Conn` when the dashboard mounts.
|
||||
|
||||
|
|
@ -283,6 +320,7 @@ defmodule Oban.Web.Resolver do
|
|||
* `:scale_queues`
|
||||
* `:stop_queues`
|
||||
* `:update_crons`
|
||||
* `:update_jobs`
|
||||
|
||||
Actions which aren't listed are considered disabled.
|
||||
|
||||
|
|
@ -411,6 +449,7 @@ defmodule Oban.Web.Resolver do
|
|||
@optional_callbacks format_job_args: 1,
|
||||
format_job_meta: 1,
|
||||
format_recorded: 2,
|
||||
format_signal: 2,
|
||||
bulk_action_limit: 1,
|
||||
hint_query_limit: 1,
|
||||
jobs_query_limit: 1,
|
||||
|
|
@ -451,6 +490,15 @@ defmodule Oban.Web.Resolver do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decode an awaitable signal payload from a compressed, base64 binary into proper terms.
|
||||
|
||||
Signals share the same wire format as recorded output, so this is a thin alias around
|
||||
`decode_recorded/2` provided for documentation symmetry.
|
||||
"""
|
||||
@spec decode_signal(binary(), [:safe]) :: term()
|
||||
def decode_signal(bin, opts \\ [:safe]), do: decode_recorded(bin, opts)
|
||||
|
||||
@doc false
|
||||
def call_with_fallback(resolver, fun, args) when is_atom(fun) and is_list(args) do
|
||||
resolver =
|
||||
|
|
@ -482,6 +530,13 @@ defmodule Oban.Web.Resolver do
|
|||
|> inspect(@inspect_opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_signal(signal, _job) do
|
||||
signal
|
||||
|> decode_signal()
|
||||
|> inspect(@inspect_opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def resolve_user(_conn), do: nil
|
||||
|
||||
|
|
|
|||
1
vendor/oban_web/lib/oban/web/router.ex
vendored
|
|
@ -200,7 +200,6 @@ defmodule Oban.Web.Router do
|
|||
get "/css-:md5", Oban.Web.Assets, :css, as: :oban_web_asset
|
||||
get "/js-:md5", Oban.Web.Assets, :js, as: :oban_web_asset
|
||||
get "/fonts/inter.woff2", Oban.Web.Assets, :font, as: :oban_web_font
|
||||
get "/icons/*path", Oban.Web.Assets, :icon, as: :oban_web_icon
|
||||
|
||||
live "/", Oban.Web.DashboardLive, :home, route_opts
|
||||
live "/:page", Oban.Web.DashboardLive, :index, route_opts
|
||||
|
|
|
|||
13
vendor/oban_web/lib/oban/web/utils.ex
vendored
|
|
@ -5,9 +5,9 @@ defmodule Oban.Web.Utils do
|
|||
|
||||
alias Oban.Repo
|
||||
|
||||
def has_crons?(conf), do: has_table?(conf, "oban_crons")
|
||||
def has_crons?(conf), do: has_table?("oban_crons", conf)
|
||||
|
||||
def has_workflows?(conf), do: has_table?(conf, "oban_workflows")
|
||||
def has_workflows?(conf), do: has_table?("oban_workflows", conf)
|
||||
|
||||
def has_pro? do
|
||||
persistent_cache(:pro?, fn -> Code.ensure_loaded?(Oban.Pro) end)
|
||||
|
|
@ -20,10 +20,13 @@ defmodule Oban.Web.Utils do
|
|||
end
|
||||
end
|
||||
|
||||
defp has_table?(conf, table_name) do
|
||||
%{name: name, prefix: prefix} = conf
|
||||
defp has_table?(_table_name, %{engine: Oban.Engines.Dolphin}), do: false
|
||||
defp has_table?(_table_name, %{engine: Oban.Engines.Lite}), do: false
|
||||
|
||||
persistent_cache({:table?, name, table_name}, fn ->
|
||||
defp has_table?(table_name, conf) do
|
||||
%{name: oban_name, prefix: prefix} = conf
|
||||
|
||||
persistent_cache({:table?, oban_name, table_name}, fn ->
|
||||
query =
|
||||
from("tables")
|
||||
|> put_query_prefix("information_schema")
|
||||
|
|
|
|||
4
vendor/oban_web/mix.exs
vendored
|
|
@ -2,7 +2,7 @@ defmodule Oban.Web.MixProject do
|
|||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/oban-bg/oban_web"
|
||||
@version "2.12.1"
|
||||
@version "2.12.5"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
|
@ -130,7 +130,7 @@ defmodule Oban.Web.MixProject do
|
|||
|
||||
defp oban_pro_dep do
|
||||
if oban_repo_configured?() do
|
||||
[{:oban_pro, "~> 1.7.0-rc.0", repo: :oban, only: [:test, :dev]}]
|
||||
[{:oban_pro, "~> 1.7.0", repo: :oban, only: [:test, :dev]}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
|
|
|||
2
vendor/oban_web/priv/static/app.css
vendored
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 380 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 13.5V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m12-3V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m-6-9V3.75m0 3.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 9.75V10.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 373 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 211 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.678 48.678 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3l-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3l-3 3"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 339 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 241 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 211 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 302 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6L9 12.75l4.286-4.286a11.948 11.948 0 014.306 6.43l.776 2.898m0 0l3.182-5.511m-3.182 5.51l-5.511-3.181"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 284 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.49 12 3.75 3.75m0 0-3.75 3.75m3.75-3.75H3.74V4.499" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 253 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0l-3.75-3.75M17.25 21L21 17.25"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 253 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 250 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.317 6.066a.648.648 0 0 1 .337.703l-.683 3.517h3.404c.245 0 .467.147.568.375a.657.657 0 0 1-.096.688l-5.416 6.43a.613.613 0 0 1-.747.155.648.648 0 0 1-.338-.702l.683-3.518H7.625a.624.624 0 0 1-.568-.375.657.657 0 0 1 .096-.688l5.416-6.43a.613.613 0 0 1 .748-.155ZM21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 479 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.412 15.655 9.75 21.75l3.745-4.012M9.257 13.5H3.75l2.659-2.849m2.048-2.194L14.25 2.25 12 10.5h8.25l-4.707 5.043M8.457 8.457 3 3m5.457 5.457 7.086 7.086m0 0L21 21"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 340 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 719 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 669 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 326 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 235 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 197 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 203 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 226 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 201 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 202 B |