defmodule PlausibleWeb.Live.CSVExport do
@moduledoc """
LiveView allowing scheduling, watching, downloading, and deleting S3 and local exports.
"""
use PlausibleWeb, :live_view
use Phoenix.HTML
alias PlausibleWeb.Components.Generic
alias Plausible.Exports
# :not_mounted_at_router ensures we have already done auth checks in the controller
# if this liveview becomes available from the router, please make sure
# to check that current_user_role is allowed to manage site exports
@impl true
def mount(:not_mounted_at_router, session, socket) do
%{
"storage" => storage,
"site_id" => site_id,
"email_to" => email_to
} = session
socket =
socket
|> assign(site_id: site_id, email_to: email_to, storage: storage)
|> assign_new(:site, fn -> Plausible.Repo.get!(Plausible.Site, site_id) end)
|> fetch_export()
if connected?(socket) do
Exports.oban_listen()
end
{:ok, socket}
end
defp fetch_export(socket) do
%{storage: storage, site_id: site_id} = socket.assigns
get_export =
case storage do
"s3" ->
&Exports.get_s3_export/1
"local" ->
%{domain: domain, timezone: timezone} = socket.assigns.site
&Exports.get_local_export(&1, domain, timezone)
end
socket = assign(socket, export: nil)
if job = Exports.get_last_export_job(site_id) do
%Oban.Job{state: state} = job
case state do
_ when state in ["scheduled", "available", "retryable"] ->
assign(socket, status: "in_progress")
"executing" ->
# Exports.oban_notify/1 is called in `perform/1` and
# the notification arrives while the job.state is still "executing"
if export = get_export.(site_id) do
assign(socket, status: "ready", export: export)
else
assign(socket, status: "in_progress")
end
"completed" ->
if export = get_export.(site_id) do
assign(socket, status: "ready", export: export)
else
assign(socket, status: "can_schedule")
end
"discarded" ->
assign(socket, status: "failed")
"cancelled" ->
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
if export = get_export.(site_id) do
assign(socket, status: "ready", export: export)
else
assign(socket, status: "can_schedule")
end
end
else
if export = get_export.(site_id) do
assign(socket, status: "ready", export: export)
else
assign(socket, status: "can_schedule")
end
end
end
@impl true
def render(assigns) do
~H"""
<%= case @status do %>
<% "can_schedule" -> %>
<.prepare_download />
<% "in_progress" -> %>
<.in_progress />
<% "failed" -> %>
<.failed />
<% "ready" -> %>
<.download storage={@storage} export={@export} />
<% end %>
"""
end
defp prepare_download(assigns) do
~H"""
Prepare your data for download by clicking the button above. When that's done, a Zip file that you can download will appear.
""" end defp in_progress(assigns) do ~H"""The preparation of your stats might take a while. Depending on the volume of your data, it might take up to 20 minutes. Feel free to leave the page and return later.
""" end defp failed(assigns) do ~H"""Something went wrong when preparing your download. Please
Note that this file will expire <.hint message={@export.expires_at}> <%= Timex.Format.DateTime.Formatters.Relative.format!(@export.expires_at, "{relative}") %>.
Located at <.hint message={@export.path}><%= format_path(@export.path) %> (<%= format_bytes(@export.size) %>)
""" end defp hint(assigns) do ~H""" <%= render_slot(@inner_block) %> """ end @impl true def handle_event("export", _params, socket) do %{storage: storage, site_id: site_id, email_to: email_to} = socket.assigns schedule_result = case storage do "s3" -> Exports.schedule_s3_export(site_id, email_to) "local" -> Exports.schedule_local_export(site_id, email_to) end socket = case schedule_result do {:ok, _job} -> fetch_export(socket) {:error, :no_data} -> socket |> put_flash(:error, "There is no data to export") |> redirect( external: Routes.site_path(socket, :settings_imports_exports, socket.assigns.site.domain) ) end {:noreply, socket} end def handle_event("cancel", _params, socket) do if job = Exports.get_last_export_job(socket.assigns.site_id), do: Oban.cancel_job(job) {:noreply, fetch_export(socket)} end def handle_event("delete", _params, socket) do %{storage: storage, site_id: site_id} = socket.assigns case storage do "s3" -> Exports.delete_s3_export(site_id) "local" -> Exports.delete_local_export(site_id) end {:noreply, fetch_export(socket)} end @impl true def handle_info({:notification, Exports, %{"site_id" => site_id}}, socket) do socket = if site_id == socket.assigns.site_id do fetch_export(socket) else socket end {:noreply, socket} end @format_path_regex ~r/^(?