Add CRM QoL improvements around usage info and linking Paddle (#4229)

* Link subscription status to Paddle profile whenever available

* Display usage info directly in user form in CRM

* Handle pageview limit fallback gracefully when rendering subscription quota

* Remove usage page link from users list view in CRM

* Add tests for usage page

* Apply `raw/1` only to the one (and only) element of the list
This commit is contained in:
Adrian Gruntkowski 2024-06-17 09:21:24 +02:00 committed by GitHub
parent dd1d74ccb7
commit aadf528459
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 101 additions and 44 deletions

View File

@ -606,6 +606,7 @@ if config_env() in [:dev, :staging, :prod, :test] do
ecto_repo: Plausible.Repo, ecto_repo: Plausible.Repo,
router: PlausibleWeb.Router, router: PlausibleWeb.Router,
admin_title: "Plausible Admin", admin_title: "Plausible Admin",
extensions: [Plausible.CrmExtensions],
resources: [ resources: [
auth: [ auth: [
resources: [ resources: [

View File

@ -36,7 +36,6 @@ defmodule Plausible.Auth.UserAdmin do
trial_expiry_date: %{name: "Trial expiry", value: &format_date(&1.trial_expiry_date)}, trial_expiry_date: %{name: "Trial expiry", value: &format_date(&1.trial_expiry_date)},
subscription_plan: %{value: &subscription_plan/1}, subscription_plan: %{value: &subscription_plan/1},
subscription_status: %{value: &subscription_status/1}, subscription_status: %{value: &subscription_status/1},
usage: %{value: &usage_link/1},
grace_period: %{value: &grace_period_status/1}, grace_period: %{value: &grace_period_status/1},
accept_traffic_until: %{ accept_traffic_until: %{
name: "Accept traffic until", name: "Accept traffic until",
@ -110,12 +109,7 @@ defmodule Plausible.Auth.UserAdmin do
quota = PlausibleWeb.AuthView.subscription_quota(user.subscription) quota = PlausibleWeb.AuthView.subscription_quota(user.subscription)
interval = PlausibleWeb.AuthView.subscription_interval(user.subscription) interval = PlausibleWeb.AuthView.subscription_interval(user.subscription)
manage_url = {:safe, ~s(<a href="#{manage_url(user.subscription)}">#{quota} \(#{interval}\)</a>)}
Plausible.Billing.PaddleApi.vendors_domain() <>
"/subscriptions/customers/manage/" <>
user.subscription.paddle_subscription_id
{:safe, ~s(<a href="#{manage_url}">#{quota} \(#{interval}\)</a>)}
else else
"--" "--"
end end
@ -124,7 +118,13 @@ defmodule Plausible.Auth.UserAdmin do
defp subscription_status(user) do defp subscription_status(user) do
cond do cond do
user.subscription -> user.subscription ->
PlausibleWeb.AuthView.present_subscription_status(user.subscription.status) status_str = PlausibleWeb.AuthView.present_subscription_status(user.subscription.status)
if user.subscription.paddle_subscription_id do
{:safe, ~s(<a href="#{manage_url(user.subscription)}">#{status_str}</a>)}
else
status_str
end
Plausible.Users.on_trial?(user) -> Plausible.Users.on_trial?(user) ->
"On trial" "On trial"
@ -134,13 +134,9 @@ defmodule Plausible.Auth.UserAdmin do
end end
end end
on_ee do defp manage_url(%{paddle_subscription_id: paddle_id} = _subscription) do
defp usage_link(user) do Plausible.Billing.PaddleApi.vendors_domain() <>
path = PlausibleWeb.Router.Helpers.admin_path(PlausibleWeb.Endpoint, :usage, user.id) "/subscriptions/customers/manage/" <> paddle_id
{:safe, ~s(<a href="#{path}">Usage</a>)}
end
else
defp usage_link(_), do: nil
end end
defp format_date(nil), do: "--" defp format_date(nil), do: "--"

View File

@ -0,0 +1,32 @@
defmodule Plausible.CrmExtensions do
@moduledoc """
Extensions for Kaffy CRM
"""
use Plausible
on_ee do
def javascripts(%{assigns: %{context: "auth", resource: "user", entry: %{} = user}}) do
[
Phoenix.HTML.raw("""
<script type="text/javascript">
(async () => {
const response = await fetch("/crm/auth/user/#{user.id}/usage?embed=true")
const usageHTML = await response.text()
const cardBody = document.querySelector(".card-body")
if (cardBody) {
const usageDOM = document.createElement("div")
usageDOM.innerHTML = usageHTML
cardBody.prepend(usageDOM)
}
})()
</script>
""")
]
end
end
def javascripts(_) do
[]
end
end

View File

@ -17,39 +17,47 @@ defmodule PlausibleWeb.AdminController do
team_members: Quota.Limits.team_member_limit(user) team_members: Quota.Limits.team_member_limit(user)
} }
html_response = usage_and_limits_html(user, usage, limits) html_response = usage_and_limits_html(user, usage, limits, params["embed"] == "true")
conn conn
|> put_resp_content_type("text/html") |> put_resp_content_type("text/html")
|> send_resp(200, html_response) |> send_resp(200, html_response)
end end
defp usage_and_limits_html(user, usage, limits) do defp usage_and_limits_html(user, usage, limits, embed?) do
""" content = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Usage - user:#{user.id}</title>
<style>
ul, li {margin-top: 10px;}
body {padding-top: 10px;}
</style>
</head>
<body>
<ul> <ul>
<li>Sites: <b>#{usage.sites}</b> / #{limits.sites}</li> <li>Sites: <b>#{usage.sites}</b> / #{limits.sites}</li>
<li>Team members: <b>#{usage.team_members}</b> / #{limits.team_members}</li> <li>Team members: <b>#{usage.team_members}</b> / #{limits.team_members}</li>
<li>Features: #{features_usage(usage.features)}</li> <li>Features: #{features_usage(usage.features)}</li>
<li>Monthly pageviews: #{monthly_pageviews_usage(usage.monthly_pageviews, limits.monthly_pageviews)}</li> <li>Monthly pageviews: #{monthly_pageviews_usage(usage.monthly_pageviews, limits.monthly_pageviews)}</li>
</ul> </ul>
</body>
</html>
""" """
if embed? do
content
else
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Usage - user:#{user.id}</title>
<style>
ul, li {margin-top: 10px;}
body {padding-top: 10px;}
</style>
</head>
<body>
#{content}
</body>
</html>
"""
end
end end
defp features_usage(features_module_list) do defp features_usage(features_module_list) do

View File

@ -9,14 +9,20 @@ defmodule PlausibleWeb.AuthView do
def subscription_quota(nil, _options), do: "Free trial" def subscription_quota(nil, _options), do: "Free trial"
def subscription_quota(subscription, options) do def subscription_quota(subscription, options) do
subscription pageview_limit = Plausible.Billing.Quota.Limits.monthly_pageview_limit(subscription)
|> Plausible.Billing.Quota.Limits.monthly_pageview_limit()
|> PlausibleWeb.StatsView.large_number_format() quota =
|> then(fn quota -> if pageview_limit == :unlimited do
if Keyword.get(options, :format) == :long, "unlimited"
do: "#{quota} pageviews", else
else: quota PlausibleWeb.StatsView.large_number_format(pageview_limit)
end) end
if Keyword.get(options, :format) == :long do
"#{quota} pageviews"
else
quota
end
end end
def subscription_interval(subscription) do def subscription_interval(subscription) do

View File

@ -35,7 +35,7 @@ defmodule PlausibleWeb.StatsView do
"#{billions}B" "#{billions}B"
end end
true -> is_integer(n) ->
Integer.to_string(n) Integer.to_string(n)
end end
end end

View File

@ -11,6 +11,20 @@ defmodule PlausibleWeb.AdminControllerTest do
conn = get(conn, "/crm/auth/user/1/usage") conn = get(conn, "/crm/auth/user/1/usage")
assert response(conn, 403) == "Not allowed" assert response(conn, 403) == "Not allowed"
end end
@tag :ee_only
test "returns usage data as a standalone page", %{conn: conn, user: user} do
patch_env(:super_admin_user_ids, [user.id])
conn = get(conn, "/crm/auth/user/#{user.id}/usage")
assert response(conn, 200) =~ "<html"
end
@tag :ee_only
test "returns usage data in embeddable form when requested", %{conn: conn, user: user} do
patch_env(:super_admin_user_ids, [user.id])
conn = get(conn, "/crm/auth/user/#{user.id}/usage?embed=true")
refute response(conn, 200) =~ "<html"
end
end end
describe "POST /crm/sites/site/:site_id" do describe "POST /crm/sites/site/:site_id" do