Current section

Files

Jump to
oban_web lib oban web live crons new_component.ex
Raw

lib/oban/web/live/crons/new_component.ex

defmodule Oban.Web.Crons.NewComponent do
use Oban.Web, :live_component
import Oban.Web.FormComponents
alias Oban.Pro.Plugins.DynamicCron
alias Oban.Web.Timezones
@compile {:no_warn_undefined, DynamicCron}
@fields ~w(args expression max_attempts name priority queue tags timezone worker)a
@impl Phoenix.LiveComponent
def render(assigns) do
~H"""
<div
id="new-cron"
class="relative z-50 hidden"
phx-mounted={show_drawer()}
phx-remove={hide_drawer()}
phx-window-keydown="keydown"
phx-target={@myself}
>
<div
id="new-cron-bg"
class="bg-zinc-50/80 dark:bg-zinc-950/80 fixed inset-0 hidden transition-opacity"
aria-hidden="true"
phx-click="close"
phx-target={@myself}
/>
<div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden">
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
<div
id="new-cron-panel"
class="pointer-events-auto w-screen max-w-md hidden transition-transform translate-x-full"
>
<div class="flex h-full flex-col overflow-y-scroll bg-white dark:bg-gray-900 shadow-xl">
<div class="flex items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
New Dynamic Cron
</h2>
<button
type="button"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 cursor-pointer"
phx-click="close"
phx-target={@myself}
aria-label="Close"
>
<Icons.x_mark class="h-6 w-6" />
</button>
</div>
<form
id="new-cron-form"
class="flex-1 px-4 py-6 space-y-4"
phx-change="form-change"
phx-submit="create-cron"
phx-target={@myself}
>
<.form_field
label="Worker"
name="worker"
value={@form.worker}
placeholder="MyApp.Workers.SomeWorker"
required={true}
/>
<.form_field
label="Name"
name="name"
value={@form.name}
placeholder="some-worker"
required={true}
/>
<.form_field
label="Expression"
name="expression"
value={@form.expression}
placeholder="* * * * *"
required={true}
/>
<.select_field
label="Queue"
name="queue"
value={@form.queue}
options={queue_options(@queues)}
/>
<.select_field
label="Timezone"
name="timezone"
value={@form.timezone}
options={Timezones.options_with_blank()}
/>
<div class="grid grid-cols-2 gap-4">
<.form_field
label="Priority"
name="priority"
value={@form.priority}
type="number"
placeholder="0"
/>
<.form_field
label="Max Attempts"
name="max_attempts"
value={@form.max_attempts}
type="number"
placeholder="20"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<.form_field label="Tags" name="tags" value={@form.tags} placeholder="tag1, tag2" />
<div class="flex items-end pb-2">
<.checkbox_field
label="Guaranteed"
name="guaranteed"
checked={@form.guaranteed}
/>
</div>
</div>
<.form_field
label="Args"
name="args"
value={@form.args}
type="textarea"
placeholder="{}"
rows={1}
/>
<div class="pt-4">
<button
type="submit"
class="w-full px-4 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"
>
Create Cron
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
"""
end
# Callbacks
@impl Phoenix.LiveComponent
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign_new(:form, fn -> default_form() end)
{:ok, socket}
end
# Events
@impl Phoenix.LiveComponent
def handle_event("form-change", params, socket) do
form = socket.assigns.form
# Auto-generate name from worker if name is empty or matches previous auto-generated name
name =
if params["name"] == "" or params["name"] == form.name do
worker_to_name(params["worker"])
else
params["name"]
end
form =
@fields
|> Map.new(fn key -> {key, params[to_string(key)]} end)
|> Map.put(:name, name)
|> Map.put(:guaranteed, params["guaranteed"] == "true")
{:noreply, assign(socket, form: form)}
end
def handle_event("create-cron", params, socket) do
enforce_access!(:insert_crons, socket.assigns.access)
%{conf: conf} = socket.assigns
with {:ok, worker} <- parse_worker(params["worker"]),
{:ok, opts} <- build_opts(params) do
case DynamicCron.insert(conf.name, [{params["expression"], worker, opts}]) do
{:ok, _entries} ->
send(self(), {:flash, :info, "Cron '#{params["name"]}' created successfully"})
{:noreply, push_patch(socket, to: oban_path(:crons))}
{:error, reason} ->
send(self(), {:flash, :error, "Failed to create cron: #{inspect(reason)}"})
{:noreply, socket}
end
else
{:error, message} ->
send(self(), {:flash, :error, message})
{:noreply, socket}
end
end
def handle_event("close", _params, socket) do
{:noreply, push_patch(socket, to: oban_path(:crons))}
end
def handle_event("keydown", %{"key" => "Escape"}, socket) do
{:noreply, push_patch(socket, to: oban_path(:crons))}
end
def handle_event("keydown", _params, socket) do
{:noreply, socket}
end
# JS Commands
defp show_drawer do
%JS{}
|> JS.show(to: "#new-cron")
|> JS.show(
to: "#new-cron-bg",
transition: {"ease-out duration-300", "opacity-0", "opacity-100"}
)
|> JS.show(
to: "#new-cron-panel",
transition: {"ease-out duration-300", "translate-x-full", "translate-x-0"}
)
|> JS.add_class("overflow-hidden", to: "body")
end
defp hide_drawer do
%JS{}
|> JS.hide(
to: "#new-cron-bg",
transition: {"ease-in duration-200", "opacity-100", "opacity-0"}
)
|> JS.hide(
to: "#new-cron-panel",
time: 200,
transition: {"ease-in duration-200", "translate-x-0", "translate-x-full"}
)
|> JS.hide(to: "#new-cron", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
end
# Helpers
defp default_form do
@fields
|> Map.new(&{&1, ""})
|> Map.put(:guaranteed, false)
end
defp worker_to_name(""), do: ""
defp worker_to_name(worker) do
worker
|> String.split(".")
|> List.last()
|> Macro.underscore()
|> String.replace("_", "-")
end
defp parse_worker(worker) when is_binary(worker) and worker != "" do
{:ok, String.trim(worker)}
end
defp parse_worker(_), do: {:error, "Worker is required"}
defp build_opts(params) do
opts = [name: params["name"]]
opts =
if params["guaranteed"] == "true" do
Keyword.put(opts, :guaranteed, true)
else
opts
end
opts =
if params["queue"] != "" do
Keyword.put(opts, :queue, params["queue"])
else
opts
end
opts =
if params["timezone"] != "" do
Keyword.put(opts, :timezone, params["timezone"])
else
opts
end
opts =
case parse_int(params["priority"]) do
nil -> opts
priority -> Keyword.put(opts, :priority, priority)
end
opts =
case parse_int(params["max_attempts"]) do
nil -> opts
max_attempts -> Keyword.put(opts, :max_attempts, max_attempts)
end
opts =
case parse_tags(params["tags"]) do
nil -> opts
tags -> Keyword.put(opts, :tags, tags)
end
opts =
case parse_json(params["args"]) do
nil -> opts
args -> Keyword.put(opts, :args, args)
end
{:ok, opts}
end
end