defmodule PhoenixKit.Modules.Sync.Web.History do
@moduledoc """
LiveView for DB Sync transfer history.
Displays all data transfers with filtering and approval workflow support.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Modules.Sync
alias PhoenixKit.Modules.Sync.Transfers
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
@per_page 20
@impl true
def mount(params, _session, socket) do
locale = params["locale"] || "en"
project_title = Settings.get_setting("project_title", "PhoenixKit")
config = Sync.get_config()
socket =
socket
|> assign(:page_title, "Transfer History")
|> assign(:project_title, project_title)
|> assign(:current_locale, locale)
|> assign(:current_path, Routes.path("/admin/sync/history", locale: locale))
|> assign(:config, config)
|> assign(:page, 1)
|> assign(:direction_filter, nil)
|> assign(:status_filter, nil)
|> assign(:show_approval_modal, false)
|> assign(:selected_transfer, nil)
|> load_transfers()
{:ok, socket}
end
@impl true
def handle_params(params, _url, socket) do
page = String.to_integer(params["page"] || "1")
direction_filter = params["direction"]
status_filter = params["status"]
socket =
socket
|> assign(:page, page)
|> assign(:direction_filter, direction_filter)
|> assign(:status_filter, status_filter)
|> load_transfers()
{:noreply, socket}
end
defp load_transfers(socket) do
page = socket.assigns.page
direction = socket.assigns.direction_filter
status = socket.assigns.status_filter
opts = [
limit: @per_page,
offset: (page - 1) * @per_page,
preload: [:connection]
]
opts = if direction, do: Keyword.put(opts, :direction, direction), else: opts
opts = if status, do: Keyword.put(opts, :status, status), else: opts
transfers = Transfers.list_transfers(opts)
total_count = Transfers.count_transfers(Keyword.take(opts, [:direction, :status]))
total_pages = max(1, ceil(total_count / @per_page))
pending_count = Transfers.count_transfers(status: "pending_approval")
socket
|> assign(:transfers, transfers)
|> assign(:total_count, total_count)
|> assign(:total_pages, total_pages)
|> assign(:pending_approval_count, pending_count)
end
@impl true
def handle_event("filter", %{"direction" => direction, "status" => status}, socket) do
query_params = %{}
query_params =
if direction != "", do: Map.put(query_params, "direction", direction), else: query_params
query_params =
if status != "", do: Map.put(query_params, "status", status), else: query_params
base_path = Routes.path("/admin/sync/history")
path =
if map_size(query_params) > 0 do
base_path <> "?" <> URI.encode_query(query_params)
else
base_path
end
{:noreply, push_patch(socket, to: path)}
end
def handle_event("clear_filters", _params, socket) do
path = Routes.path("/admin/sync/history")
{:noreply, push_patch(socket, to: path)}
end
def handle_event("show_approval_modal", %{"id" => id}, socket) do
transfer = Transfers.get_transfer_with_preloads(String.to_integer(id), preload: [:connection])
socket =
socket
|> assign(:show_approval_modal, true)
|> assign(:selected_transfer, transfer)
{:noreply, socket}
end
def handle_event("close_approval_modal", _params, socket) do
socket =
socket
|> assign(:show_approval_modal, false)
|> assign(:selected_transfer, nil)
{:noreply, socket}
end
def handle_event("approve_transfer", %{"id" => id}, socket) do
transfer = Transfers.get_transfer!(String.to_integer(id))
current_user = socket.assigns.phoenix_kit_current_scope.user
case Transfers.approve_transfer(transfer, current_user.id) do
{:ok, _transfer} ->
socket =
socket
|> put_flash(:info, "Transfer approved successfully")
|> assign(:show_approval_modal, false)
|> assign(:selected_transfer, nil)
|> load_transfers()
{:noreply, socket}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to approve transfer")}
end
end
def handle_event("deny_transfer", %{"transfer_id" => id, "reason" => reason}, socket) do
transfer = Transfers.get_transfer!(String.to_integer(id))
current_user = socket.assigns.phoenix_kit_current_scope.user
reason = if reason == "", do: nil, else: reason
case Transfers.deny_transfer(transfer, current_user.id, reason) do
{:ok, _transfer} ->
socket =
socket
|> put_flash(:info, "Transfer denied")
|> assign(:show_approval_modal, false)
|> assign(:selected_transfer, nil)
|> load_transfers()
{:noreply, socket}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to deny transfer")}
end
end
def handle_event("page", %{"page" => page}, socket) do
query_params = %{"page" => page}
query_params =
if socket.assigns.direction_filter,
do: Map.put(query_params, "direction", socket.assigns.direction_filter),
else: query_params
query_params =
if socket.assigns.status_filter,
do: Map.put(query_params, "status", socket.assigns.status_filter),
else: query_params
base_path = Routes.path("/admin/sync/history")
path = base_path <> "?" <> URI.encode_query(query_params)
{:noreply, push_patch(socket, to: path)}
end
@impl true
def render(assigns) do
~H"""
View and manage data transfers
Transfer History
<%= if Enum.empty?(@transfers) do %>
Direction
Table
Records
Status
Connection
Date
Actions
<% else %>
<%= for transfer <- @transfers do %>
No transfers found
<% end %>
<% end %>
<.direction_badge direction={transfer.direction} />
{transfer.table_name}
<.record_counts transfer={transfer} />
<.status_badge status={transfer.status} />
<%= if transfer.connection do %>
{transfer.connection.name}
<% else %>
Session
<% end %>
<.time_ago datetime={transfer.inserted_at} />
<%= if transfer.status == "pending_approval" do %>
<% else %>
<% end %>
<.direction_badge direction={@transfer.direction} />
{@transfer.table_name}
{@transfer.records_requested || 0}
{@transfer.records_transferred}
{format_bytes(@transfer.bytes_transferred)}
{@transfer.records_created}
{@transfer.records_updated}
{@transfer.records_skipped}
{@transfer.records_failed}
{@transfer.connection.name}
{@transfer.connection.site_url}
{@transfer.requester_ip || "Unknown"}
{@transfer.conflict_strategy || "default"}
{Calendar.strftime(@transfer.inserted_at, "%Y-%m-%d %H:%M:%S")}
{Calendar.strftime(@transfer.started_at, "%Y-%m-%d %H:%M:%S")}
{Calendar.strftime(@transfer.completed_at, "%Y-%m-%d %H:%M:%S")}