diff --git a/lib/plausible/teams/billing.ex b/lib/plausible/teams/billing.ex index dacf595c6fd5..b483d56c1323 100644 --- a/lib/plausible/teams/billing.ex +++ b/lib/plausible/teams/billing.ex @@ -219,7 +219,7 @@ defmodule Plausible.Teams.Billing do @doc """ Returns the number of sites the given team owns. """ - @spec site_usage(Teams.Team.t()) :: non_neg_integer() + @spec site_usage(Teams.Team.t() | nil) :: non_neg_integer() def site_usage(nil), do: 0 def site_usage(team) do @@ -350,7 +350,7 @@ defmodule Plausible.Teams.Billing do team owns. Alternatively, given an optional argument of `site_ids`, the usage from across all those sites is queried instead. """ - @spec monthly_pageview_usage(Teams.Team.t(), list() | nil) :: monthly_pageview_usage() + @spec monthly_pageview_usage(Teams.Team.t() | nil, list() | nil) :: monthly_pageview_usage() def monthly_pageview_usage(team, site_ids \\ nil) def monthly_pageview_usage(team, nil) do @@ -376,7 +376,7 @@ defmodule Plausible.Teams.Billing do end end - @spec team_member_usage(Teams.Team.t(), Keyword.t()) :: non_neg_integer() + @spec team_member_usage(Teams.Team.t() | nil, Keyword.t()) :: non_neg_integer() @doc """ Returns the total count of team members associated with the team's sites. @@ -430,7 +430,7 @@ defmodule Plausible.Teams.Billing do pageviews: pageviews, custom_events: custom_events, total: pageviews + custom_events, - sites: per_site_usage(owned_site_ids, date_range) + per_site: per_site_usage(owned_site_ids, date_range) } end @@ -470,7 +470,7 @@ defmodule Plausible.Teams.Billing do pageviews: pageviews, custom_events: custom_events, total: pageviews + custom_events, - sites: per_site_usage(owned_site_ids, date_range) + per_site: per_site_usage(owned_site_ids, date_range) } end diff --git a/lib/plausible_web/components/billing/billing.ex b/lib/plausible_web/components/billing/billing.ex index 95bc7a1116a7..dc5e503ea57b 100644 --- a/lib/plausible_web/components/billing/billing.ex +++ b/lib/plausible_web/components/billing/billing.ex @@ -4,6 +4,8 @@ defmodule PlausibleWeb.Components.Billing do use PlausibleWeb, :component use Plausible + import PlausibleWeb.Components.Icons + require Plausible.Billing.Subscription.Status alias Plausible.Billing.{Plan, Plans, EnterprisePlan} @@ -49,6 +51,10 @@ defmodule PlausibleWeb.Components.Billing do """ end + attr :usage, :map, required: true + attr :limit, :any, required: true + attr :total_pageview_usage_domain, :string, default: nil + def render_monthly_pageview_usage(%{usage: usage} = assigns) when is_map_key(usage, :last_30_days) do ~H""" @@ -57,6 +63,7 @@ defmodule PlausibleWeb.Components.Billing do limit={@limit} period={:last_30_days} expanded={true} + total_pageview_usage_domain={@total_pageview_usage_domain} /> """ end @@ -75,7 +82,8 @@ defmodule PlausibleWeb.Components.Billing do usage={@usage.current_cycle} limit={@limit} period={:current_cycle} - expanded={not @show_all and Enum.empty?(@usage.current_cycle.sites)} + expanded={not @show_all and Enum.empty?(@usage.current_cycle.per_site)} + total_pageview_usage_domain={@total_pageview_usage_domain} /> <%= if @show_all do %> <.monthly_pageview_usage_breakdown @@ -83,12 +91,14 @@ defmodule PlausibleWeb.Components.Billing do limit={@limit} period={:last_cycle} expanded={false} + total_pageview_usage_domain={@total_pageview_usage_domain} /> <.monthly_pageview_usage_breakdown usage={@usage.penultimate_cycle} limit={@limit} period={:penultimate_cycle} expanded={false} + total_pageview_usage_domain={@total_pageview_usage_domain} /> <% end %> @@ -99,8 +109,19 @@ defmodule PlausibleWeb.Components.Billing do attr(:limit, :any, required: true) attr(:period, :atom, required: true) attr(:expanded, :boolean, required: true) + attr(:total_pageview_usage_domain, :string, default: nil) defp monthly_pageview_usage_breakdown(assigns) do + assigns = + assign( + assigns, + :total_link, + dashboard_url( + assigns.total_pageview_usage_domain, + assigns.usage.date_range + ) + ) + ~H"""
@@ -120,6 +141,17 @@ defmodule PlausibleWeb.Components.Billing do class="size-4 transition-transform" x-bind:class="open ? 'rotate-90' : ''" /> Total billable pageviews + <.tooltip :if={@total_link} centered?={true}> + <:tooltip_content>View billing period in dashboard + <.link + href={@total_link} + class="text-indigo-500 hover:text-indigo-600" + data-test-id="total-pageviews-dashboard-link" + x-on:click.stop + > + <.external_link_icon class="ml-0.5 size-3.5 [&_path]:stroke-2" /> + + {PlausibleWeb.TextHelpers.number_format(@usage.total)} @@ -130,23 +162,34 @@ defmodule PlausibleWeb.Components.Billing do
<.pageview_usage_row id={"pageviews_#{@period}"} - label={if Enum.empty?(@usage.sites), do: "Pageviews", else: "Total pageviews"} + label={if Enum.empty?(@usage.per_site), do: "Pageviews", else: "Total pageviews"} value={@usage.pageviews} /> <.pageview_usage_row id={"custom_events_#{@period}"} - label={if Enum.empty?(@usage.sites), do: "Custom events", else: "Total custom events"} + label={if Enum.empty?(@usage.per_site), do: "Custom events", else: "Total custom events"} value={@usage.custom_events} />
-
+

0} class="border-gray-200 dark:border-gray-700" />
- {site.domain} + + {site.domain} + <.tooltip centered?={true}> + <:tooltip_content>View billing period in dashboard + <.link + href={dashboard_url(site.domain, @usage.date_range)} + class="shrink-0 text-indigo-500 hover:text-indigo-600" + > + <.external_link_icon class="ml-0.5 size-3.5 [&_path]:stroke-2" /> + + + {PlausibleWeb.TextHelpers.number_format(site.total)}
<.pageview_usage_row label="Pageviews" value={site.pageviews} /> @@ -174,6 +217,15 @@ defmodule PlausibleWeb.Components.Billing do """ end + defp dashboard_url(nil, _date_range), do: nil + + defp dashboard_url(domain, date_range) do + base = Routes.stats_path(PlausibleWeb.Endpoint, :stats, domain, []) + + base <> + "?period=custom&from=#{Date.to_iso8601(date_range.first)}&to=#{Date.to_iso8601(date_range.last)}" + end + defp cycle_label(:current_cycle), do: "(current cycle)" defp cycle_label(:last_30_days), do: "(last 30 days)" diff --git a/lib/plausible_web/controllers/settings_controller.ex b/lib/plausible_web/controllers/settings_controller.ex index 2fdc8d72826d..aa39e396ad04 100644 --- a/lib/plausible_web/controllers/settings_controller.ex +++ b/lib/plausible_web/controllers/settings_controller.ex @@ -152,6 +152,14 @@ defmodule PlausibleWeb.SettingsController do notification_type = Plausible.Billing.Quota.usage_notification_type(team, usage) + total_pageview_usage_domain = + if site_usage == 1 do + [site] = Plausible.Teams.owned_sites(team) + site.domain + else + on_ee(do: team && get_consolidated_view_domain(team), else: nil) + end + render(conn, :subscription, layout: {PlausibleWeb.LayoutView, :settings}, subscription: subscription, @@ -162,7 +170,8 @@ defmodule PlausibleWeb.SettingsController do site_limit: Teams.Billing.site_limit(team), team_member_limit: Teams.Billing.team_member_limit(team), team_member_usage: team_member_usage, - notification_type: notification_type + notification_type: notification_type, + total_pageview_usage_domain: total_pageview_usage_domain ) end @@ -461,6 +470,15 @@ defmodule PlausibleWeb.SettingsController do end end + on_ee do + defp get_consolidated_view_domain(team) do + case Plausible.ConsolidatedView.get(team) do + nil -> nil + view -> if Plausible.ConsolidatedView.ok_to_display?(team), do: view.domain + end + end + end + defp handle_email_updated(conn) do conn |> put_flash(:success, "Email updated") diff --git a/lib/plausible_web/templates/settings/subscription.html.heex b/lib/plausible_web/templates/settings/subscription.html.heex index e97f7b17f90c..b7a3e563e7e5 100644 --- a/lib/plausible_web/templates/settings/subscription.html.heex +++ b/lib/plausible_web/templates/settings/subscription.html.heex @@ -117,6 +117,7 @@
diff --git a/test/plausible/billing/quota_test.exs b/test/plausible/billing/quota_test.exs index a080d2a97822..2e2ee5770b01 100644 --- a/test/plausible/billing/quota_test.exs +++ b/test/plausible/billing/quota_test.exs @@ -973,15 +973,15 @@ defmodule Plausible.Billing.QuotaTest do build(:event, timestamp: ~N[2023-05-15 00:00:00], name: "pageview") ]) - %{sites: sites} = Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, today) + %{per_site: per_site} = Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, today) - assert length(sites) == 2 + assert length(per_site) == 2 assert %{pageviews: 1, custom_events: 1, total: 2} = - Enum.find(sites, &(&1.domain == site1.domain)) + Enum.find(per_site, &(&1.domain == site1.domain)) assert %{pageviews: 1, custom_events: 0, total: 1} = - Enum.find(sites, &(&1.domain == site2.domain)) + Enum.find(per_site, &(&1.domain == site2.domain)) end test "sites with zero events in the period still appear in the breakdown" do @@ -995,12 +995,12 @@ defmodule Plausible.Billing.QuotaTest do build(:event, timestamp: ~N[2023-05-15 00:00:00], name: "pageview") ]) - %{sites: sites} = Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, today) + %{per_site: per_site} = Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, today) - assert length(sites) == 2 + assert length(per_site) == 2 assert %{pageviews: 0, custom_events: 0, total: 0} = - Enum.find(sites, &(&1.domain == site2.domain)) + Enum.find(per_site, &(&1.domain == site2.domain)) end test "returns empty sites list when team has only one site" do @@ -1009,7 +1009,8 @@ defmodule Plausible.Billing.QuotaTest do team = team_of(user) today = ~D[2023-06-01] - assert %{sites: []} = Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, today) + assert %{per_site: []} = + Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, today) end test "returns empty sites list when team has more than 10 sites" do @@ -1018,7 +1019,8 @@ defmodule Plausible.Billing.QuotaTest do team = team_of(user) today = ~D[2023-06-01] - assert %{sites: []} = Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, today) + assert %{per_site: []} = + Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, today) end end diff --git a/test/plausible_web/components/billing/billing_test.exs b/test/plausible_web/components/billing/billing_test.exs index cbf9d7ad1f4c..93ee24f79c3a 100644 --- a/test/plausible_web/components/billing/billing_test.exs +++ b/test/plausible_web/components/billing/billing_test.exs @@ -184,7 +184,7 @@ defmodule PlausibleWeb.Components.BillingTest do custom_events: 0, total: 0, date_range: Date.range(~D[2024-01-01], ~D[2024-01-31]), - sites: [] + per_site: [] } test "only shows current cycle when neither last nor current cycle is exceeded" do @@ -257,7 +257,7 @@ defmodule PlausibleWeb.Components.BillingTest do test "shows 'Total pageviews' and 'Total custom events' labels when per-site breakdown is present" do cycle_with_sites = %{ @cycle - | sites: [ + | per_site: [ %{domain: "example.com", pageviews: 100, custom_events: 50, total: 150}, %{domain: "app.example.com", pageviews: 200, custom_events: 30, total: 230} ] @@ -274,7 +274,7 @@ defmodule PlausibleWeb.Components.BillingTest do test "renders per-site breakdown when sites are present" do cycle_with_sites = %{ @cycle - | sites: [ + | per_site: [ %{domain: "example.com", pageviews: 100, custom_events: 50, total: 150}, %{domain: "app.example.com", pageviews: 200, custom_events: 30, total: 230} ] @@ -308,7 +308,7 @@ defmodule PlausibleWeb.Components.BillingTest do test "current cycle is not expanded by default when per-site breakdown is present" do cycle_with_sites = %{ @cycle - | sites: [ + | per_site: [ %{domain: "example.com", pageviews: 100, custom_events: 50, total: 150}, %{domain: "app.example.com", pageviews: 200, custom_events: 30, total: 230} ] @@ -320,6 +320,55 @@ defmodule PlausibleWeb.Components.BillingTest do refute html =~ "{ open: true }" end + + test "renders a total link when total_pageview_usage_domain is provided" do + usage = %{current_cycle: @cycle, last_cycle: @cycle, penultimate_cycle: @cycle} + + html = + render_monthly_pageview_usage(usage, 10_000, + total_pageview_usage_domain: "my-site.example.com" + ) + + assert element_exists?(html, "[data-test-id='total-pageviews-dashboard-link']") + assert html =~ "/my-site.example.com/?period=custom" + end + + test "renders no total link when no domain is provided" do + usage = %{current_cycle: @cycle, last_cycle: @cycle, penultimate_cycle: @cycle} + + html = render_monthly_pageview_usage(usage, 10_000) + + refute element_exists?(html, "[data-test-id='total-pageviews-dashboard-link']") + end + + test "per-site breakdown shows a dashboard link for each site" do + cycle_with_sites = %{ + @cycle + | per_site: [ + %{domain: "example.com", pageviews: 100, custom_events: 50, total: 150}, + %{domain: "app.example.com", pageviews: 200, custom_events: 30, total: 230} + ] + } + + usage = %{current_cycle: cycle_with_sites, last_cycle: @cycle, penultimate_cycle: @cycle} + + html = render_monthly_pageview_usage(usage, 10_000) + + assert html =~ "/example.com/?period=custom" + assert html =~ "/app.example.com/?period=custom" + end + + test "dashboard links include the billing cycle date range" do + usage = %{current_cycle: @cycle, last_cycle: @cycle, penultimate_cycle: @cycle} + + html = + render_monthly_pageview_usage(usage, 10_000, + total_pageview_usage_domain: "my-site.example.com" + ) + + assert html =~ + "/my-site.example.com/?period=custom&from=2024-01-01&to=2024-01-31" + end end defp render_progress_bar(usage, limit) do @@ -334,13 +383,18 @@ defmodule PlausibleWeb.Components.BillingTest do |> rendered_to_string() end - defp render_monthly_pageview_usage(usage, limit) do - assigns = %{usage: usage, limit: limit} + defp render_monthly_pageview_usage(usage, limit, opts \\ []) do + assigns = %{ + usage: usage, + limit: limit, + total_pageview_usage_domain: opts[:total_pageview_usage_domain] + } ~H""" """ |> rendered_to_string() diff --git a/test/plausible_web/controllers/settings_controller_test.exs b/test/plausible_web/controllers/settings_controller_test.exs index 48ea0d5287d5..102b54c60b32 100644 --- a/test/plausible_web/controllers/settings_controller_test.exs +++ b/test/plausible_web/controllers/settings_controller_test.exs @@ -532,6 +532,57 @@ defmodule PlausibleWeb.SettingsControllerTest do assert html =~ "Invoices" assert text(html) =~ "We couldn't retrieve your invoices" end + + @tag :ee_only + test "shows dashboard link to the site when team has exactly one site", %{ + conn: conn, + user: user + } do + new_site(owner: user) + + html = + conn + |> get(Routes.settings_path(conn, :subscription)) + |> html_response(200) + + assert element_exists?(html, "[data-test-id='total-pageviews-dashboard-link']") + end + + @tag :ee_only + test "shows no total dashboard link when team has multiple sites and no consolidated view", %{ + conn: conn, + user: user + } do + new_site(owner: user) + new_site(owner: user) + + html = + conn + |> get(Routes.settings_path(conn, :subscription)) + |> html_response(200) + + refute element_exists?(html, "[data-test-id='total-pageviews-dashboard-link']") + end + + on_ee do + test "shows consolidated view dashboard link when team has a consolidated view", %{ + conn: conn, + user: user + } do + new_site(owner: user) + new_site(owner: user) + team = team_of(user) + new_consolidated_view(team) + + html = + conn + |> set_current_team(team) + |> get(Routes.settings_path(conn, :subscription)) + |> html_response(200) + + assert element_exists?(html, "[data-test-id='total-pageviews-dashboard-link']") + end + end end describe "GET /security" do