Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,16 @@ tasks:
migrate:
cmds:
- mix ecto.migrate

deploy:
desc: "Deploy the application to Docker Swarm (usage: task deploy --tag v0.1.0)"
vars:
tag:
sh: 'echo ${tag:-}'
cmds:
- |
if [ -z "{{.tag}}" ]; then
echo "Error: tag is not provided! Example: task deploy --tag v0.1.0"
exit 1
fi
TAG_NAME={{.tag}} envsubst < infra/docker-compose.prod.yml | docker stack deploy -c - croniq
4 changes: 3 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import "phoenix_html"
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar"
import ToggleFields from "./toggle_fields.js"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken }
params: { _csrf_token: csrfToken },
hooks: { ToggleFields }
})

// Show progress bar on live navigation and form submits
Expand Down
17 changes: 17 additions & 0 deletions assets/js/toggle_fields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const ToggleFields = {
mounted() {
this.updateVisibility();
},

updated() {
this.updateVisibility();
},

updateVisibility() {
const shouldShow = this.el.dataset.show === "true";
console.log("ToggleFields: shouldShow =", shouldShow, "for element:", this.el.id);
this.el.style.display = shouldShow ? "block" : "none";
}
};

export default ToggleFields;
20 changes: 14 additions & 6 deletions lib/croniq_web/controllers/tasks_html/tasks.html.heex
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Cron Tasks</h1>
<.link
href={~p"/tasks/new"}
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg transition-colors"
>
+ Create New Task
</.link>
<div class="flex space-x-2">
<.link
href={~p"/create-task"}
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors"
>
+ Create Task (LiveView)
</.link>
<.link
href={~p"/tasks/new"}
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg transition-colors"
>
+ Create Task (Classic)
</.link>
</div>
</div>

<%= if @current_user && !@current_user.confirmed_at do %>
Expand Down
112 changes: 112 additions & 0 deletions lib/croniq_web/live/task_form_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
defmodule CroniqWeb.TaskFormLive do
use CroniqWeb, :live_view
alias Croniq.Task

def mount(_params, _session, socket) do
if socket.assigns.current_user.confirmed_at do
headers_string = Jason.encode!(%{})

changeset =
Task.changeset(%Task{}, %{"headers" => headers_string, "task_type" => "recurring"})

{:ok,
assign(socket,
changeset: changeset,
task_type: "recurring",
show_recurring_fields: true,
show_delayed_fields: false
)}
else
{:ok,
socket
|> put_flash(:error, "Please confirm your email address before creating tasks.")
|> redirect(to: ~p"/tasks")}
end
end

def handle_event("validate", %{"task" => task_params}, socket) do
task_type = Map.get(task_params, "task_type", "recurring")

changeset =
%Task{}
|> Task.changeset(task_params)
|> Map.put(:action, :validate)

{:noreply,
assign(socket,
changeset: changeset,
task_type: task_type,
show_recurring_fields: task_type == "recurring",
show_delayed_fields: task_type == "delayed"
)}
end

def handle_event("save", %{"task" => task_params}, socket) do
case get_task_type(task_params) do
"delayed" ->
create_delayed_task(socket, task_params)

_ ->
create_recurring_task(socket, task_params)
end
end

def handle_event("toggle_task_type", %{"task_type" => task_type}, socket) do
IO.puts("Toggle task type to: #{task_type}")

changeset =
socket.assigns.changeset
|> Ecto.Changeset.put_change(:task_type, task_type)
|> Map.put(:action, :validate)

show_recurring = task_type == "recurring"
show_delayed = task_type == "delayed"

IO.puts("show_recurring_fields: #{show_recurring}, show_delayed_fields: #{show_delayed}")

{:noreply,
assign(socket,
changeset: changeset,
task_type: task_type,
show_recurring_fields: show_recurring,
show_delayed_fields: show_delayed
)}
end

defp get_task_type(%{"task_type" => task_type}), do: task_type
defp get_task_type(%{"scheduled_at" => _}), do: "delayed"
defp get_task_type(_), do: "recurring"

defp create_delayed_task(socket, task_params) do
case Croniq.Task.create_delayed_task(socket.assigns.current_user.id, task_params) do
{:ok, task} ->
{:noreply,
socket
|> put_flash(
:info,
"Delayed task created successfully! Will execute at #{format_datetime(task.scheduled_at)}"
)
|> push_navigate(to: ~p"/tasks/#{task.id}/edit")}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end

defp create_recurring_task(socket, task_params) do
case Croniq.Task.create_task(socket.assigns.current_user.id, task_params) do
{:ok, task} ->
{:noreply,
socket
|> put_flash(:info, "Recurring task created successfully!")
|> push_navigate(to: ~p"/tasks/#{task.id}/edit")}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end

defp format_datetime(datetime) do
Calendar.strftime(datetime, "%Y-%m-%d %H:%M:%S UTC")
end
end
155 changes: 155 additions & 0 deletions lib/croniq_web/live/task_form_live.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<div class="max-w-2xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-6">Create New Task</h1>

<.form
:let={f}
for={@changeset}
phx-change="validate"
phx-submit="save"
id="task-form"
class="space-y-6 bg-white p-6 rounded-lg shadow-md border border-gray-200"
>
<div class="space-y-6">
<!-- Task Type Selection -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Task Type</label>
<div class="flex space-x-4">
<label class="flex items-center">
<input
type="radio"
name="task[task_type]"
value="recurring"
checked={@task_type == "recurring"}
class="mr-2"
/>
<span>Recurring Task</span>
</label>
<label class="flex items-center">
<input
type="radio"
name="task[task_type]"
value="delayed"
checked={@task_type == "delayed"}
class="mr-2"
/>
<span>Delayed Task</span>
</label>
</div>
</div>

<!-- Common Fields -->
<div>
<label class="block text-gray-700 mb-1">Task Name*</label>
<.input
type="text"
field={f[:name]}
class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
required
/>
</div>

<div>
<label class="block text-gray-700 mb-1">Status</label>
<.input
field={f[:status]}
type="select"
options={[{"Enabled", "active"}, {"Disabled", "disabled"}]}
class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>

<div>
<label class="block text-gray-700 mb-1">URL*</label>
<.input
type="url"
field={f[:url]}
placeholder="https://api.example.com/endpoint"
class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
required
/>
</div>

<div>
<.input
field={f[:method]}
type="select"
label="HTTP Method*"
options={["GET", "POST", "PUT", "DELETE"]}
class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
required
/>
</div>

<div>
<.input
field={
%{
f[:headers]
| value:
if(is_map(f[:headers].value),
do: Jason.encode!(f[:headers].value),
else: f[:headers].value
)
}
}
type="textarea"
label="Headers (JSON)"
class="w-full px-3 py-2 border rounded-lg font-mono h-20 focus:ring-2 focus:ring-blue-500"
placeholder='{"Content-Type": "application/json", "Authorization": "Basic token"}'
/>
</div>

<div>
<.input
field={f[:body]}
type="textarea"
label="Request Body"
class="w-full px-3 py-2 border rounded-lg font-mono h-20 focus:ring-2 focus:ring-blue-500"
placeholder='{"key": "value"}'
data-test="request-body-input"
/>
</div>

<!-- Conditional Fields -->
<div
id="recurring-fields"
class="space-y-4"
style={if @show_recurring_fields, do: "display: block;", else: "display: none;"}
>
<div>
<.input
field={f[:schedule]}
type="text"
label="Cron Schedule"
placeholder="*/5 * * * *"
/>
</div>
</div>

<div
id="delayed-fields"
class="space-y-4"
style={if @show_delayed_fields, do: "display: block;", else: "display: none;"}
>
<div>
<.input field={f[:scheduled_at]} type="datetime-local" label="Execute At" />
</div>
</div>

<div class="flex justify-end space-x-4 pt-4">
<.link
href={~p"/tasks"}
class="px-4 py-2 border rounded-lg text-gray-700 hover:bg-gray-50"
>
Cancel
</.link>
<button
type="submit"
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg"
>
Create Task
</button>
</div>
</div>
</.form>
</div>
3 changes: 2 additions & 1 deletion lib/croniq_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ defmodule CroniqWeb.Router do
post "/", TasksController, :create
get "/new", TasksController, :new_task
get "/:task_id/edit", TasksController, :edit_form
get "/:task_id", TasksController, :task_details
# get "/:task_id", TasksController, :task_details
put "/:task_id", TasksController, :edit
# TODO: make post
delete "/:task_id", TasksController, :delete
Expand Down Expand Up @@ -124,6 +124,7 @@ defmodule CroniqWeb.Router do
on_mount: [{CroniqWeb.UserAuth, :ensure_authenticated}] do
live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
live "/create-task", TaskFormLive, :new
end
end

Expand Down
Loading
Loading