diff --git a/assets/js/dashboard.tsx b/assets/js/dashboard.tsx index dce77243d5fa..c4451df210d9 100644 --- a/assets/js/dashboard.tsx +++ b/assets/js/dashboard.tsx @@ -68,7 +68,7 @@ if (container && container.dataset) { team: { identifier: container.dataset.teamIdentifier ?? null, hasConsolidatedView: - container.dataset.teamHasConsolidatedView === 'true' + container.dataset.consolidatedViewAvailable === 'true' } } : { diff --git a/assets/js/liveview/live_socket.js b/assets/js/liveview/live_socket.js index 135372826d89..c0f723f8cf29 100644 --- a/assets/js/liveview/live_socket.js +++ b/assets/js/liveview/live_socket.js @@ -6,7 +6,7 @@ import 'phoenix_html' import { Socket } from 'phoenix' import { LiveSocket } from 'phoenix_live_view' -import { Modal } from 'prima' +import { Modal, Dropdown } from 'prima' import topbar from 'topbar' /* eslint-enable import/no-unresolved */ @@ -15,7 +15,7 @@ import Alpine from 'alpinejs' let csrfToken = document.querySelector("meta[name='csrf-token']") let websocketUrl = document.querySelector("meta[name='websocket-url']") if (csrfToken && websocketUrl) { - let Hooks = { Modal } + let Hooks = { Modal, Dropdown } Hooks.Metrics = { mounted() { this.handleEvent('send-metrics', ({ event_name }) => { diff --git a/extra/lib/plausible/consolidated_view.ex b/extra/lib/plausible/consolidated_view.ex index 7db9d072c966..82bd8a08e2bb 100644 --- a/extra/lib/plausible/consolidated_view.ex +++ b/extra/lib/plausible/consolidated_view.ex @@ -16,18 +16,45 @@ defmodule Plausible.ConsolidatedView do import Ecto.Query - @spec ok_to_display?(Team.t() | nil, User.t() | nil) :: boolean() - def ok_to_display?(team, user) do - with %Team{} <- team, - %User{} <- user, - true <- Plausible.Auth.is_super_admin?(user), - true <- enabled?(team), - true <- has_sites_to_consolidate?(team) do - true - else - _ -> - false - end + @spec flag_enabled?(Team.t()) :: boolean() + def flag_enabled?(team) do + FunWithFlags.enabled?(:consolidated_view, for: team) + end + + @spec cta_dismissed?(User.t(), Team.t()) :: boolean() + def cta_dismissed?(%User{} = user, %Team{} = team) do + {:ok, team_membership} = Teams.Memberships.get_team_membership(team, user) + Teams.Memberships.get_preference(team_membership, :consolidated_view_cta_dismissed) + end + + @spec dismiss_cta(User.t(), Team.t()) :: :ok + def dismiss_cta(%User{} = user, %Team{} = team) do + {:ok, team_membership} = Teams.Memberships.get_team_membership(team, user) + Teams.Memberships.set_preference(team_membership, :consolidated_view_cta_dismissed, true) + + :ok + end + + @spec restore_cta(User.t(), Team.t()) :: :ok + def restore_cta(%User{} = user, %Team{} = team) do + {:ok, team_membership} = Teams.Memberships.get_team_membership(team, user) + + Teams.Memberships.set_preference( + team_membership, + :consolidated_view_cta_dismissed, + false + ) + + :ok + end + + @spec ok_to_display?(Team.t() | nil) :: boolean() + def ok_to_display?(team) do + is_struct(team, Team) and + flag_enabled?(team) and + view_enabled?(team) and + has_sites_to_consolidate?(team) and + Plausible.Billing.Feature.ConsolidatedView.check_availability(team) == :ok end @spec reset_if_enabled(Team.t()) :: :ok @@ -56,14 +83,25 @@ defmodule Plausible.ConsolidatedView do from(s in q, where: s.consolidated == true) end - @spec enable(Team.t()) :: {:ok, Site.t()} | {:error, :no_sites | :team_not_setup} + @spec enable(Team.t()) :: + {:ok, Site.t()} | {:error, :no_sites | :team_not_setup | :upgrade_required} def enable(%Team{} = team) do - with :ok <- ensure_eligible(team), do: do_enable(team) - end + cond do + not has_sites_to_consolidate?(team) -> + {:error, :no_sites} - @spec enabled?(Team.t()) :: boolean() - def enabled?(%Team{} = team) do - not is_nil(get(team)) + not Teams.setup?(team) -> + {:error, :team_not_setup} + + not flag_enabled?(team) -> + {:error, :unavailable} + + true -> + case Plausible.Billing.Feature.ConsolidatedView.check_availability(team) do + :ok -> do_enable(team) + error -> error + end + end end @spec disable(Team.t()) :: :ok @@ -159,16 +197,6 @@ defmodule Plausible.ConsolidatedView do team.identifier end - # TODO: Only active trials and business subscriptions should be eligible. - # This function should also call a new underlying feature module. - defp ensure_eligible(%Team{} = team) do - cond do - not Teams.setup?(team) -> {:error, :team_not_setup} - not has_sites_to_consolidate?(team) -> {:error, :no_sites} - true -> :ok - end - end - defp native_stats_start_at(%Team{} = team) do q = from(sr in Site.regular(), @@ -181,7 +209,7 @@ defmodule Plausible.ConsolidatedView do end defp has_sites_to_consolidate?(%Team{} = team) do - Teams.owned_sites_count(team) > 0 + Teams.owned_sites_count(team) > 1 end defp majority_sites_timezone(%Team{} = team) do @@ -200,4 +228,8 @@ defmodule Plausible.ConsolidatedView do nil -> "Etc/UTC" end end + + defp view_enabled?(%Team{} = team) do + not is_nil(get(team)) + end end diff --git a/extra/lib/plausible_web/live/customer_support/team/components/consolidated_views.ex b/extra/lib/plausible_web/live/customer_support/team/components/consolidated_views.ex index 6eb0cbe33a5c..bab4180bd945 100644 --- a/extra/lib/plausible_web/live/customer_support/team/components/consolidated_views.ex +++ b/extra/lib/plausible_web/live/customer_support/team/components/consolidated_views.ex @@ -45,6 +45,7 @@ defmodule PlausibleWeb.CustomerSupport.Team.Components.ConsolidatedViews do <:thead> <.th>Domain <.th>Timezone + <.th>Available? <.th invisible>Dashboard <.th invisible>24H <.th invisible>Delete @@ -53,6 +54,7 @@ defmodule PlausibleWeb.CustomerSupport.Team.Components.ConsolidatedViews do <:tbody :let={consolidated_view}> <.td>{consolidated_view.domain} <.td>{consolidated_view.timezone} + <.td>{availability(@team)} <.td> <.styled_link new_tab={true} @@ -74,7 +76,7 @@ defmodule PlausibleWeb.CustomerSupport.Team.Components.ConsolidatedViews do <.delete_button phx-click="delete-consolidated-view" phx-target={@myself} - data-confirm="Are you sure you want to delete this consolidated view?" + data-confirm="Are you sure you want to delete this consolidated view? All existing consolidated view configuration will be lost. The view itself will be recreated whenever eligible subscription/trial accesses /sites for that team." /> @@ -101,4 +103,11 @@ defmodule PlausibleWeb.CustomerSupport.Team.Components.ConsolidatedViews do success("Deleted consolidated view") {:noreply, assign(socket, consolidated_views: [])} end + + defp availability(team) do + case Plausible.Billing.Feature.ConsolidatedView.check_availability(team) do + :ok -> "Yes" + {:error, :upgrade_required} -> "No - upgrade required" + end + end end diff --git a/lib/plausible/billing/feature.ex b/lib/plausible/billing/feature.ex index 2117406509da..69554d638e6f 100644 --- a/lib/plausible/billing/feature.ex +++ b/lib/plausible/billing/feature.ex @@ -71,7 +71,8 @@ defmodule Plausible.Billing.Feature do Plausible.Billing.Feature.SiteSegments, Plausible.Billing.Feature.SitesAPI, Plausible.Billing.Feature.StatsAPI, - Plausible.Billing.Feature.SSO + Plausible.Billing.Feature.SSO, + Plausible.Billing.Feature.ConsolidatedView ] # Generate a union type for features @@ -229,6 +230,15 @@ defmodule Plausible.Billing.Feature.SSO do display_name: "Single Sign-On" end +defmodule Plausible.Billing.Feature.ConsolidatedView do + use Plausible + + @moduledoc false + use Plausible.Billing.Feature, + name: :consolidated_view, + display_name: "Consolidated View" +end + defmodule Plausible.Billing.Feature.Teams do @moduledoc """ Unlike other feature modules, this one only exists to make feature gating diff --git a/lib/plausible/teams/memberships.ex b/lib/plausible/teams/memberships.ex index 00d84f3de013..8478f7c1a653 100644 --- a/lib/plausible/teams/memberships.ex +++ b/lib/plausible/teams/memberships.ex @@ -7,6 +7,8 @@ defmodule Plausible.Teams.Memberships do alias Plausible.Repo alias Plausible.Teams + require Teams.Memberships.UserPreference + @spec all(Teams.Team.t(), Keyword.t()) :: [Teams.Membership.t()] def all(team, opts \\ []) do exclude_guests? = Keyword.get(opts, :exclude_guests?, false) @@ -223,6 +225,35 @@ defmodule Plausible.Teams.Memberships do end end + @spec set_preference(Teams.Membership.t(), atom(), any()) :: + Teams.Memberships.UserPreference.t() + def set_preference(team_membership, option, value) + when option in Teams.Memberships.UserPreference.options() do + team_membership + |> Teams.Memberships.UserPreference.changeset(%{option => value}) + |> Repo.insert!( + conflict_target: [:team_membership_id], + on_conflict: + from(p in Teams.Memberships.UserPreference, update: [set: [{^option, ^value}]]), + returning: true + ) + end + + @spec get_preference(Teams.Membership.t(), atom()) :: any() + def get_preference(team_membership, option) + when option in Teams.Memberships.UserPreference.options() do + defaults = %Teams.Memberships.UserPreference{} + + query = + from( + tup in Teams.Memberships.UserPreference, + where: tup.team_membership_id == ^team_membership.id, + select: field(tup, ^option) + ) + + Repo.one(query) || Map.fetch!(defaults, option) + end + defp get_guest_membership(site_id, user_id) do query = from( diff --git a/lib/plausible/teams/memberships/user_preference.ex b/lib/plausible/teams/memberships/user_preference.ex new file mode 100644 index 000000000000..2996fcc1c0b2 --- /dev/null +++ b/lib/plausible/teams/memberships/user_preference.ex @@ -0,0 +1,28 @@ +defmodule Plausible.Teams.Memberships.UserPreference do + @moduledoc """ + Team-specific user preferences schema + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t() :: %__MODULE__{} + + @options [:consolidated_view_cta_dismissed] + + schema "team_membership_user_preferences" do + field :consolidated_view_cta_dismissed, :boolean, default: false + + belongs_to :team_membership, Plausible.Teams.Membership + + timestamps() + end + + defmacro options, do: @options + + def changeset(team_membership, attrs \\ %{}) do + %__MODULE__{} + |> cast(attrs, @options) + |> put_assoc(:team_membership, team_membership) + end +end diff --git a/lib/plausible_web/components/generic.ex b/lib/plausible_web/components/generic.ex index 94ee011400bf..466f88096529 100644 --- a/lib/plausible_web/components/generic.ex +++ b/lib/plausible_web/components/generic.ex @@ -1020,7 +1020,7 @@ defmodule PlausibleWeb.Components.Generic do def filter_bar(assigns) do ~H""" -
+
@@ -1031,7 +1031,7 @@ defmodule PlausibleWeb.Components.Generic do type="text" name="filter-text" id="filter-text" - class="w-36 sm:w-full pl-8 text-sm dark:bg-gray-750 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block border-gray-300 dark:border-gray-750 rounded-md dark:placeholder:text-gray-400 focus:outline-none focus:ring-3 focus:ring-indigo-500/20 dark:focus:ring-indigo-500/25 focus:border-indigo-500" + class="w-full max-w-64 pl-8 text-sm dark:bg-gray-750 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block border-gray-300 dark:border-gray-750 rounded-md dark:placeholder:text-gray-400 focus:outline-none focus:ring-3 focus:ring-indigo-500/20 dark:focus:ring-indigo-500/25 focus:border-indigo-500" placeholder="Press / to search" x-ref="filter_text" phx-debounce={200} diff --git a/lib/plausible_web/components/prima_dropdown.ex b/lib/plausible_web/components/prima_dropdown.ex new file mode 100644 index 000000000000..07bdad1fba8a --- /dev/null +++ b/lib/plausible_web/components/prima_dropdown.ex @@ -0,0 +1,47 @@ +defmodule PlausibleWeb.Components.PrimaDropdown do + @moduledoc false + alias Prima.Dropdown + use Phoenix.Component + + @dropdown_item_icon_base_class "text-gray-600 dark:text-gray-400 group-hover/item:text-gray-900 group-data-focus/item:text-gray-900 dark:group-hover/item:text-gray-100 dark:group-data-focus/item:text-gray-100" + + defdelegate dropdown(assigns), to: Prima.Dropdown + defdelegate dropdown_trigger(assigns), to: Prima.Dropdown + + slot(:inner_block, required: true) + + # placement: bottom-end should probably be default in prima. Feels more natural + # for dropdown menus than bottom-start which is the current default + def dropdown_menu(assigns) do + ~H""" + + {render_slot(@inner_block)} + + """ + end + + attr(:as, :any, default: nil) + attr(:disabled, :boolean, default: false) + attr(:rest, :global, include: ~w(navigate patch href)) + slot(:inner_block, required: true) + + def dropdown_item(assigns) do + ~H""" + + {render_slot(@inner_block)} + + """ + end + + def dropdown_item_icon_class(size \\ "size-4") do + "#{size} #{@dropdown_item_icon_base_class}" + end +end diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index f7abca33282a..07dc1412b10c 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -63,8 +63,8 @@ defmodule PlausibleWeb.StatsController do consolidated_view? = Plausible.Sites.consolidated?(site) - team_has_consolidated_view? = - on_ee(do: Plausible.ConsolidatedView.ok_to_display?(site.team, current_user), else: false) + consolidated_view_available? = + on_ee(do: Plausible.ConsolidatedView.ok_to_display?(site.team), else: false) team_identifier = site.team.identifier @@ -74,6 +74,9 @@ defmodule PlausibleWeb.StatsController do {:ok, segments} = Plausible.Segments.get_all_for_site(site, site_role) cond do + consolidated_view? and not consolidated_view_available? and site_role != :super_admin -> + redirect(conn, to: Routes.site_path(conn, :index)) + (stats_start_date && can_see_stats?) || (can_see_stats? && skip_to_dashboard?) -> flags = get_flags(current_user, site) @@ -96,7 +99,7 @@ defmodule PlausibleWeb.StatsController do load_dashboard_js: true, hide_footer?: if(ce?() || demo, do: false, else: site_role != :public), consolidated_view?: consolidated_view?, - team_has_consolidated_view?: team_has_consolidated_view?, + consolidated_view_available?: consolidated_view_available?, team_identifier: team_identifier ) @@ -402,13 +405,7 @@ defmodule PlausibleWeb.StatsController do embedded? = conn.params["embed"] == "true" - consolidated_view? = Plausible.Sites.consolidated?(shared_link.site) - - team_has_consolidated_view? = - on_ee( - do: Plausible.ConsolidatedView.ok_to_display?(shared_link.site.team, current_user), - else: false - ) + true = Plausible.Sites.regular?(shared_link.site) team_identifier = shared_link.site.team.identifier @@ -435,8 +432,9 @@ defmodule PlausibleWeb.StatsController do segments: segments, load_dashboard_js: true, hide_footer?: if(ce?(), do: embedded?, else: embedded? || site_role != :public), - consolidated_view?: consolidated_view?, - team_has_consolidated_view?: team_has_consolidated_view?, + # no shared links for consolidated views + consolidated_view?: false, + consolidated_view_available?: false, team_identifier: team_identifier ) end diff --git a/lib/plausible_web/live/sites.ex b/lib/plausible_web/live/sites.ex index fe3246493305..44ebd2eab160 100644 --- a/lib/plausible_web/live/sites.ex +++ b/lib/plausible_web/live/sites.ex @@ -11,6 +11,8 @@ defmodule PlausibleWeb.Live.Sites do alias Plausible.Sites alias Plausible.Teams + alias PlausibleWeb.Components.PrimaDropdown + def mount(params, _session, socket) do team = socket.assigns.current_team user = socket.assigns.current_user @@ -69,31 +71,57 @@ defmodule PlausibleWeb.Live.Sites do @needs_to_upgrade == {:needs_to_upgrade, :no_active_trial_or_subscription} } /> -
-

+
+

{Teams.name(@current_team)} - <.unstyled_link - :if={Teams.setup?(@current_team)} - data-test-id="team-settings-link" - href={Routes.settings_path(@socket, :team_general)} - > -

+ <.unstyled_link + :if={Teams.setup?(@current_team)} + data-test-id="team-settings-link" + href={Routes.settings_path(@socket, :team_general)} + class="shrink-0" + > +
-
+
<.search_form :if={@has_sites?} filter_text={@filter_text} uri={@uri} />

You don't have any sites yet.

- + + + Add + + + + + + Add website + + + + Add consolidated view + + + + + + Add website +

@@ -115,9 +143,22 @@ defmodule PlausibleWeb.Live.Sites do

    - + <.consolidated_view_card_cta + :if={ + @filter_text == "" and + !@consolidated_view and @no_consolidated_view_reason not in [:no_sites, :unavailable] and + not @consolidated_view_cta_dismissed? + } + can_manage_consolidated_view?={@can_manage_consolidated_view?} + no_consolidated_view_reason={@no_consolidated_view_reason} + current_user={@current_user} + current_team={@current_team} + /> <.consolidated_view_card - :if={@consolidated_view && consolidated_view_ok_to_display?(@current_team, @current_user)} + :if={ + @filter_text == "" and not is_nil(@consolidated_view) and + consolidated_view_ok_to_display?(@current_team) + } can_manage_consolidated_view?={@can_manage_consolidated_view?} consolidated_view={@consolidated_view} consolidated_stats={@consolidated_stats} @@ -190,29 +231,84 @@ defmodule PlausibleWeb.Live.Sites do """ end - def upgrade_card(assigns) do + def consolidated_view_card_cta(assigns) do ~H""" -
  • +
  • -

    +

    Introducing

    -

    - consolidated view +

    + Consolidated view

    -

    - See stats for all your sites in one single dashboard. -

    -
    - - Upgrade - - - Learn more - + +
    +

    + To create a consolidated view, you'll need to set up a team. +

    +
    + <.button_link + href={Routes.team_setup_path(PlausibleWeb.Endpoint, :setup)} + mt?={false} + > + Create team + + <.button_link + theme="secondary" + href="https://plausible.io/docs/consolidated-views" + mt?={false} + > + Learn more + +
    +
    + +
    +

    + Upgrade to the Business plan and set up a team to enable consolidated views. +

    + +

    + Available on Business plans. Contact your team owner to create it. +

    + +
    + <.button_link + :if={@can_manage_consolidated_view?} + href={PlausibleWeb.Router.Helpers.billing_url(PlausibleWeb.Endpoint, :choose_plan)} + mt?={false} + > + Upgrade + + + <.button_link + theme="secondary" + href="https://plausible.io/docs/consolidated-views" + mt?={false} + > + Learn more + +
    - + + +
  • """ end @@ -221,11 +317,11 @@ defmodule PlausibleWeb.Live.Sites do ~H"""
  • <.unstyled_link href={"/#{URI.encode_www_form(@consolidated_view.domain)}"} - class="flex flex-col justify-between gap-6 h-full" + class="flex flex-col justify-between gap-6 h-full bg-white p-6 dark:bg-gray-900 rounded-md shadow-sm cursor-pointer hover:shadow-lg transition-shadow duration-150" >
    @@ -238,7 +334,7 @@ defmodule PlausibleWeb.Live.Sites do
    - <.unstyled_link href={"/#{URI.encode_www_form(@site.domain)}"}> + <.unstyled_link href={"/#{URI.encode_www_form(@site.domain)}"} class="block">
    <.favicon domain={@site.domain} /> @@ -411,7 +507,7 @@ defmodule PlausibleWeb.Live.Sites do href={"/#{URI.encode_www_form(@site.domain)}/settings/general"} class="group/item !flex items-center gap-x-2" > - + Settings @@ -433,13 +529,13 @@ defmodule PlausibleWeb.Live.Sites do <.icon_pin :if={@site.pinned_at} filled={true} - class="size-[1.15rem] text-indigo-600 dark:text-indigo-500 group-hover/item:text-indigo-700 dark:group-hover/item:text-indigo-400 transition-colors duration-150" + class="size-[1.15rem] text-indigo-600 dark:text-indigo-500 group-hover/item:text-indigo-700 dark:group-hover/item:text-indigo-400" /> Unpin site <.icon_pin :if={!@site.pinned_at} - class="size-5 text-gray-600 dark:text-gray-400 group-hover/item:text-gray-900 dark:group-hover/item:text-gray-100 transition-colors duration-150" + class="size-5 text-gray-600 dark:text-gray-400 group-hover/item:text-gray-900 dark:group-hover/item:text-gray-100" /> Pin site @@ -490,7 +586,7 @@ defmodule PlausibleWeb.Live.Sites do
    - + def percentage_change(assigns) do ~H""" -

    +

    0} xmlns="http://www.w3.org/2000/svg" @@ -781,6 +877,28 @@ defmodule PlausibleWeb.Live.Sites do {:noreply, socket} end + on_ee do + def handle_event("consolidated-view-cta-dismiss", _, socket) do + :ok = + Plausible.ConsolidatedView.dismiss_cta( + socket.assigns.current_user, + socket.assigns.current_team + ) + + {:noreply, assign(socket, :consolidated_view_cta_dismissed?, true)} + end + + def handle_event("consolidated-view-cta-restore", _, socket) do + :ok = + Plausible.ConsolidatedView.restore_cta( + socket.assigns.current_user, + socket.assigns.current_team + ) + + {:noreply, assign(socket, :consolidated_view_cta_dismissed?, false)} + end + end + defp load_sites(%{assigns: assigns} = socket) do sites = Sites.list_with_invitations(assigns.current_user, assigns.params, @@ -912,32 +1030,54 @@ defmodule PlausibleWeb.Live.Sites do :sha |> :crypto.hash(domain) |> Base.encode16() end - @no_consolidated_view %{ - consolidated_view: nil, - can_manage_consolidated_view?: false, - consolidated_stats: nil - } + def no_consolidated_view(overrides \\ []) do + [ + consolidated_view: nil, + can_manage_consolidated_view?: false, + consolidated_stats: nil, + no_consolidated_view_reason: nil, + consolidated_view_cta_dismissed?: false + ] + |> Keyword.merge(overrides) + end on_ee do alias Plausible.ConsolidatedView - defp consolidated_view_ok_to_display?(team, user) do - ConsolidatedView.ok_to_display?(team, user) + defp consolidated_view_ok_to_display?(team) do + ConsolidatedView.ok_to_display?(team) end - defp init_consolidated_view_assigns(_user, nil), do: @no_consolidated_view + defp init_consolidated_view_assigns(_user, nil) do + # technically this is team not setup, but is also equivalent of having no sites at this moment (can have invitations though), so CTA should not be shown + no_consolidated_view(no_consolidated_view_reason: :no_sites) + end defp init_consolidated_view_assigns(user, team) do - if Teams.setup?(team) do - view = ConsolidatedView.get(team) - - %{ - consolidated_view: view, - can_manage_consolidated_view?: ConsolidatedView.can_manage?(user, team), - consolidated_stats: :loading - } + if ConsolidatedView.flag_enabled?(team) do + case ConsolidatedView.enable(team) do + {:ok, view} -> + %{ + consolidated_view: view, + can_manage_consolidated_view?: ConsolidatedView.can_manage?(user, team), + consolidated_stats: :loading, + no_consolidated_view_reason: nil, + consolidated_view_cta_dismissed?: ConsolidatedView.cta_dismissed?(user, team) + } + + {:error, reason} -> + no_consolidated_view( + no_consolidated_view_reason: reason, + can_manage_consolidated_view?: ConsolidatedView.can_manage?(user, team), + consolidated_view_cta_dismissed?: ConsolidatedView.cta_dismissed?(user, team) + ) + end else - @no_consolidated_view + no_consolidated_view( + no_consolidated_view_reason: :unavailable, + can_manage_consolidated_view?: ConsolidatedView.can_manage?(user, team), + consolidated_view_cta_dismissed?: ConsolidatedView.cta_dismissed?(user, team) + ) end end @@ -949,8 +1089,11 @@ defmodule PlausibleWeb.Live.Sites do end end else - defp consolidated_view_ok_to_display?(_team, _user), do: false - defp init_consolidated_view_assigns(_user, _team), do: @no_consolidated_view + defp consolidated_view_ok_to_display?(_team), do: false + + defp init_consolidated_view_assigns(_user, _team), + do: no_consolidated_view(no_consolidated_view_reason: :unavailable) + defp load_consolidated_stats(_consolidated_view), do: nil end end diff --git a/lib/plausible_web/plugins/api/schemas/capabilities.ex b/lib/plausible_web/plugins/api/schemas/capabilities.ex index 1fe152a92630..f98daee979ce 100644 --- a/lib/plausible_web/plugins/api/schemas/capabilities.ex +++ b/lib/plausible_web/plugins/api/schemas/capabilities.ex @@ -37,7 +37,8 @@ defmodule PlausibleWeb.Plugins.API.Schemas.Capabilities do SiteSegments: false, Teams: false, SharedLinks: false, - SSO: false + SSO: false, + ConsolidatedView: false } } }) diff --git a/lib/plausible_web/templates/stats/stats.html.heex b/lib/plausible_web/templates/stats/stats.html.heex index 14c034d907f8..56dd48b0afee 100644 --- a/lib/plausible_web/templates/stats/stats.html.heex +++ b/lib/plausible_web/templates/stats/stats.html.heex @@ -50,7 +50,7 @@ Plausible.Stats.Interval.valid_by_period(site: @site) |> Jason.encode!() } data-is-consolidated-view={Jason.encode!(@consolidated_view?)} - data-team-has-consolidated-view={Jason.encode!(@team_has_consolidated_view?)} + data-consolidated-view-available={Jason.encode!(@consolidated_view_available?)} data-team-identifier={@team_identifier} >

    diff --git a/mix.exs b/mix.exs index b68bd8abee32..ccee086569e4 100644 --- a/mix.exs +++ b/mix.exs @@ -114,7 +114,7 @@ defmodule Plausible.MixProject do {:phoenix_live_view, "~> 1.1"}, {:php_serializer, "~> 2.0"}, {:plug, "~> 1.13", override: true}, - {:prima, "~> 0.1.7"}, + {:prima, "~> 0.1.8"}, {:plug_cowboy, "~> 2.3"}, {:polymorphic_embed, "~> 5.0"}, {:postgrex, "~> 0.19.0"}, diff --git a/mix.lock b/mix.lock index 65fd7cc76461..523ec8712d57 100644 --- a/mix.lock +++ b/mix.lock @@ -124,7 +124,7 @@ "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.16", "e42f95337b912a73a1c4ddb077af2eb13491712d7ab79b67e13de4237dfcac50", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f2a0093895b8ef4880af76d41de4a9cf7cff6c66ad130e15a70bdabc4d279feb"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.17", "1d782b5901cf13b137c6d8c56542ff6cb618359b2adca7e185b21df728fa0c6c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fa82307dd9305657a8236d6b48e60ef2e8d9f742ee7ed832de4b8bcb7e0e5ed2"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_storybook": {:hex, :phoenix_storybook, "0.9.3", "4f94e731d4c40d4dd7d1eddf7d5c6914366da7d78552dc565b222e4036d0d76f", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: true]}, {:makeup_eex, "~> 2.0.2", [hex: :makeup_eex, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.2.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.8.1", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "4c8658b756fd8238f7e8e4343a0f12bdb91d4eba592b1c4e8118b37b6fd43e4b"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, @@ -135,7 +135,7 @@ "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "polymorphic_embed": {:hex, :polymorphic_embed, "5.0.3", "37444e0af941026a2c29b0539b6471bdd6737a6492a19264bf2bb0118e3ac242", [:mix], [{:attrs, "~> 0.6", [hex: :attrs, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "2fed44f57abf0a0fc7642e0eb0807a55b65de1562712cc0620772cbbb80e49c1"}, "postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"}, - "prima": {:hex, :prima, "0.1.7", "197fe29aa1cb6cc0c38fddb7daca017b9d4cfc1aef0b30694e02e45fcf22710f", [:mix], [{:esbuild, "~> 0.7", [hex: :esbuild, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "662ea3f8698358087c1632db8969050a74df101e202d026e2f26bc3f740737f9"}, + "prima": {:hex, :prima, "0.1.8", "1f57fb7000046bb463b2a31200b138dd10c86fd78a289f1e947a970f740b68e0", [:mix], [{:esbuild, "~> 0.7", [hex: :esbuild, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "b559ee8213a8302fd40520cc55b0fcafbc884d471d198b262dd1e0a10170de17"}, "prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"}, "public_suffix": {:git, "https://github.com/axelson/publicsuffix-elixir", "fa40c243d4b5d8598b90cff268bc4e33f3bb63f1", []}, "ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"}, @@ -165,7 +165,7 @@ "ua_inspector": {:git, "https://github.com/plausible/ua_inspector.git", "25cba4c910e80d7c34bbb1bbb939372260d088e8", [branch: "sanitize-pre"]}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, "x509": {:hex, :x509, "0.9.1", "c92026a17b7d93f19029842ca218f82ec1f1e7cc9d4aa0c48327ee778f7f482e", [:mix], [], "hexpm", "99328951a1480cfd7b1b8aa688f857f7f5bbea03077b78cad211fb3b30c2f4a8"}, "xml_builder": {:hex, :xml_builder, "2.4.0", "b20d23077266c81f593360dc037ea398461dddb6638a329743da6c73afa56725", [:mix], [], "hexpm", "833e325bb997f032b5a1b740d2fd6feed3c18ca74627f9f5f30513a9ae1a232d"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, diff --git a/priv/plans_v3.json b/priv/plans_v3.json index 55f4a03ee5da..37aa5f0d422a 100644 --- a/priv/plans_v3.json +++ b/priv/plans_v3.json @@ -134,7 +134,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ] }, { @@ -152,7 +153,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ] }, { @@ -170,7 +172,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ] }, { @@ -188,7 +191,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ] }, { @@ -206,7 +210,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ] }, { @@ -224,7 +229,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ] }, { @@ -242,7 +248,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ] }, { @@ -260,7 +267,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ] } -] \ No newline at end of file +] diff --git a/priv/plans_v4.json b/priv/plans_v4.json index 36e7f0a352d8..de39f7ac369b 100644 --- a/priv/plans_v4.json +++ b/priv/plans_v4.json @@ -126,7 +126,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -145,7 +146,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -164,7 +166,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -183,7 +186,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -202,7 +206,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -221,7 +226,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -240,7 +246,8 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -259,8 +266,9 @@ "funnels", "stats_api", "site_segments", - "shared_links" + "shared_links", + "consolidated_view" ], "data_retention_in_years": 5 } -] \ No newline at end of file +] diff --git a/priv/plans_v5.json b/priv/plans_v5.json index 00158886805e..69c3641a2329 100644 --- a/priv/plans_v5.json +++ b/priv/plans_v5.json @@ -238,7 +238,8 @@ "props", "stats_api", "revenue_goals", - "funnels" + "funnels", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -257,7 +258,8 @@ "props", "stats_api", "revenue_goals", - "funnels" + "funnels", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -276,7 +278,8 @@ "props", "stats_api", "revenue_goals", - "funnels" + "funnels", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -295,7 +298,8 @@ "props", "stats_api", "revenue_goals", - "funnels" + "funnels", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -314,7 +318,8 @@ "props", "stats_api", "revenue_goals", - "funnels" + "funnels", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -333,7 +338,8 @@ "props", "stats_api", "revenue_goals", - "funnels" + "funnels", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -352,7 +358,8 @@ "props", "stats_api", "revenue_goals", - "funnels" + "funnels", + "consolidated_view" ], "data_retention_in_years": 5 }, @@ -371,8 +378,9 @@ "props", "stats_api", "revenue_goals", - "funnels" + "funnels", + "consolidated_view" ], "data_retention_in_years": 5 } -] \ No newline at end of file +] diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 1522defcfcc5..7316a6c7a23f 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -13,6 +13,8 @@ use Plausible import Plausible.Teams.Test +FunWithFlags.enable(:consolidated_view) + words = for i <- 0..(:erlang.system_info(:atom_count) - 1), do: :erlang.binary_to_term(<<131, 75, i::24>>) diff --git a/test/plausible/billing/feature_test.exs b/test/plausible/billing/feature_test.exs index 2b208553280b..37c40ee85bb0 100644 --- a/test/plausible/billing/feature_test.exs +++ b/test/plausible/billing/feature_test.exs @@ -10,14 +10,15 @@ defmodule Plausible.Billing.FeatureTest do Funnels, RevenueGoals, StatsAPI, - Props + Props, + ConsolidatedView } @v1_growth_plan_id "558018" @v5_growth_plan_id "910429" describe "business features (for everyone)" do - for mod <- [Funnels, RevenueGoals] do + for mod <- [Funnels, RevenueGoals, ConsolidatedView] do test "#{mod}.check_availability/1 returns :ok when site owner is on a enterprise plan that supports #{mod}" do team = new_user() diff --git a/test/plausible/billing/plan_benefits_test.exs b/test/plausible/billing/plan_benefits_test.exs index 74fdd79df8fa..c2525bf31b7e 100644 --- a/test/plausible/billing/plan_benefits_test.exs +++ b/test/plausible/billing/plan_benefits_test.exs @@ -80,7 +80,8 @@ defmodule Plausible.Billing.PlanBenefitsTest do "Stats API (600 requests per hour)", "Looker Studio Connector", "Ecommerce revenue attribution", - "Funnels" + "Funnels", + "Consolidated View" ] end @@ -99,7 +100,8 @@ defmodule Plausible.Billing.PlanBenefitsTest do "Funnels", "Stats API (600 requests per hour)", "Looker Studio Connector", - "Shared Segments" + "Shared Segments", + "Consolidated View" ] end @@ -120,7 +122,8 @@ defmodule Plausible.Billing.PlanBenefitsTest do "Everything in Growth", "Ecommerce revenue attribution", "Funnels", - "Shared Segments" + "Shared Segments", + "Consolidated View" ] end end diff --git a/test/plausible/consolidated_view/cache_test.exs b/test/plausible/consolidated_view/cache_test.exs index 9a93af0765a4..2ad2722193c0 100644 --- a/test/plausible/consolidated_view/cache_test.exs +++ b/test/plausible/consolidated_view/cache_test.exs @@ -63,6 +63,7 @@ defmodule Plausible.CondolidatedView.CacheTest do owner = new_user() new_site(owner: owner, updated_at: yesterday()) + new_site(owner: owner, updated_at: yesterday()) team = team_of(owner) @@ -70,7 +71,7 @@ defmodule Plausible.CondolidatedView.CacheTest do :ok = Cache.refresh_updated_recently(cache_name: test) - assert [_] = Cache.get(consolidated_view.domain, cache_name: test, force?: true) + assert [_, _] = Cache.get(consolidated_view.domain, cache_name: test, force?: true) end test "get_from_source/1", %{test: test} do diff --git a/test/plausible/consolidated_view_test.exs b/test/plausible/consolidated_view_test.exs index 572bb12107cb..1960838e4bd3 100644 --- a/test/plausible/consolidated_view_test.exs +++ b/test/plausible/consolidated_view_test.exs @@ -8,17 +8,71 @@ defmodule Plausible.ConsolidatedViewTest do alias Plausible.ConsolidatedView alias Plausible.Teams + describe "cta state" do + setup [:create_user, :create_team] + + test "by default CTA should be shown", %{user: user, team: team} do + assert ConsolidatedView.cta_dismissed?(user, team) == false + end + + test "CTA dismissed and restored", %{user: user, team: team} do + assert :ok = ConsolidatedView.dismiss_cta(user, team) + assert ConsolidatedView.cta_dismissed?(user, team) == true + + assert :ok = ConsolidatedView.restore_cta(user, team) + assert ConsolidatedView.cta_dismissed?(user, team) == false + end + end + + describe "ok_to_display?/1" do + setup [:create_user, :create_team] + + test "returns false when team is nil" do + refute ConsolidatedView.ok_to_display?(nil) + end + + test "returns false when feature flag is disabled", %{team: team} do + FunWithFlags.disable(:consolidated_view, for_actor: team) + refute ConsolidatedView.ok_to_display?(team) + end + + test "returns false when consolidated view is not enabled", %{team: team} do + ConsolidatedView.disable(team) + refute ConsolidatedView.ok_to_display?(team) + end + + test "returns false when there are no sites to consolidate", %{team: team} do + new_site(team: team) + site = new_site(team: team) + team = Teams.complete_setup(team) + {:ok, _} = ConsolidatedView.enable(team) + Plausible.Repo.delete(site) + refute ConsolidatedView.ok_to_display?(team) + end + + test "returns true when all conditions are met", %{team: team} do + new_site(team: team) + new_site(team: team) + team = Teams.complete_setup(team) + {:ok, _} = ConsolidatedView.enable(team) + + assert ConsolidatedView.ok_to_display?(team) + end + end + describe "enable/1 and enabled?/1" do setup [:create_user, :create_team] test "creates and persists a new consolidated site instance", %{team: team} do + new_site(team: team) new_site(team: team) team = Teams.complete_setup(team) assert {:ok, %Plausible.Site{consolidated: true}} = ConsolidatedView.enable(team) - assert ConsolidatedView.enabled?(team) + assert ConsolidatedView.get(team) end test "is idempotent", %{team: team} do + new_site(team: team) new_site(team: team) team = Teams.complete_setup(team) assert {:ok, s1} = ConsolidatedView.enable(team) @@ -31,15 +85,36 @@ defmodule Plausible.ConsolidatedViewTest do assert s1.domain == s2.domain end + test "returns {:error, :upgrade_required} for ineligible subscription", %{ + team: team, + user: user + } do + subscribe_to_growth_plan(user) + new_site(team: team) + new_site(team: team) + team = Teams.complete_setup(team) + + assert ConsolidatedView.enable(team) == {:error, :upgrade_required} + end + test "returns {:error, :no_sites} when the team does not have any sites", %{team: team} do team = Teams.complete_setup(team) assert {:error, :no_sites} = ConsolidatedView.enable(team) - refute ConsolidatedView.enabled?(team) + refute ConsolidatedView.get(team) end - test "returns {:error, :team_not_setup} when the team is not set up", %{team: team} do + test "returns {:error, :team_not_setup} when the team has sites but isn't setup", %{ + team: team + } do + new_site(team: team) + new_site(team: team) assert {:error, :team_not_setup} = ConsolidatedView.enable(team) - refute ConsolidatedView.enabled?(team) + refute ConsolidatedView.get(team) + end + + test "returns {:error, :no_sites} when the team is not set up", %{team: team} do + assert {:error, :no_sites} = ConsolidatedView.enable(team) + refute ConsolidatedView.get(team) end @tag :skip @@ -66,14 +141,17 @@ defmodule Plausible.ConsolidatedViewTest do test "enable/1 updates cache", %{team: team} do team = Teams.complete_setup(team) site = new_site(team: team) + new_site(team: team) {:ok, _} = ConsolidatedView.enable(team) assert eventually(fn -> - {ConsolidatedView.Cache.get(team.identifier) == [site.id], :ok} + site_ids = ConsolidatedView.Cache.get(team.identifier) + {is_list(site_ids) and length(site_ids) == 2 and site.id in site_ids, :ok} end) end test "sets Etc/UTC by default", %{team: team} do + new_site(team: team) new_site(team: team) team = Teams.complete_setup(team) @@ -82,6 +160,7 @@ defmodule Plausible.ConsolidatedViewTest do end test "sets Etc/UTC for UTC sites", %{team: team} do + new_site(team: team, timezone: "UTC") new_site(team: team, timezone: "UTC") team = Teams.complete_setup(team) @@ -106,6 +185,7 @@ defmodule Plausible.ConsolidatedViewTest do setup [:create_user, :create_team, :create_site] setup %{team: team} do + new_site(team: team) new_consolidated_view(team) :ok end @@ -169,8 +249,11 @@ defmodule Plausible.ConsolidatedViewTest do team: team, site: site } do + new_site(team: team) new_consolidated_view(team) - assert ConsolidatedView.site_ids(team) == {:ok, [site.id]} + assert {:ok, site_ids} = ConsolidatedView.site_ids(team) + assert length(site_ids) == 2 + assert site.id in site_ids end end @@ -179,12 +262,14 @@ defmodule Plausible.ConsolidatedViewTest do test "can get by team", %{team: team} do assert is_nil(ConsolidatedView.get(team)) + new_site(team: team) new_consolidated_view(team) assert %Plausible.Site{} = ConsolidatedView.get(team) end test "can get by team.identifier", %{team: team} do assert is_nil(ConsolidatedView.get(team.identifier)) + new_site(team: team) new_consolidated_view(team) assert %Plausible.Site{} = ConsolidatedView.get(team.identifier) end @@ -196,7 +281,7 @@ defmodule Plausible.ConsolidatedViewTest do test "no-op if disabled", %{team: team} do :ok = ConsolidatedView.reset_if_enabled(team) - refute ConsolidatedView.enabled?(team) + refute ConsolidatedView.get(team) refute ConsolidatedView.get(team) end @@ -209,6 +294,13 @@ defmodule Plausible.ConsolidatedViewTest do timezone: "Europe/Warsaw" ) + _site = + new_site( + team: team, + native_stats_start_at: ~N[2024-01-01 12:00:00], + timezone: "Europe/Tiraspol" + ) + team = Teams.complete_setup(team) {:ok, first_enable} = ConsolidatedView.enable(team) @@ -223,7 +315,7 @@ defmodule Plausible.ConsolidatedViewTest do Process.sleep(1_000) :ok = ConsolidatedView.reset_if_enabled(team) - assert ConsolidatedView.enabled?(team) + assert ConsolidatedView.get(team) consolidated_view = ConsolidatedView.get(team) assert consolidated_view.native_stats_start_at == another_site.native_stats_start_at diff --git a/test/plausible/site/site_removal_test.exs b/test/plausible/site/site_removal_test.exs index d349785b8dc6..9cb7ef927899 100644 --- a/test/plausible/site/site_removal_test.exs +++ b/test/plausible/site/site_removal_test.exs @@ -57,14 +57,15 @@ defmodule Plausible.Site.SiteRemovalTest do test "site deletion disables consolidated view if need be" do owner = new_user() site = new_site(owner: owner) + new_site(owner: owner) team = team_of(owner) new_consolidated_view(team) - assert Plausible.ConsolidatedView.enabled?(team) + assert Plausible.ConsolidatedView.get(team) assert {:ok, _} = Removal.run(site) - refute Plausible.ConsolidatedView.enabled?(team) + refute Plausible.ConsolidatedView.get(team) end test "site deletion keeps consolidated view if there's still regular sites" do @@ -73,15 +74,17 @@ defmodule Plausible.Site.SiteRemovalTest do # another site new_site(owner: owner) + # third site to ensure we still have 2+ after deletion + new_site(owner: owner) team = team_of(owner) new_consolidated_view(team) - assert Plausible.ConsolidatedView.enabled?(team) + assert Plausible.ConsolidatedView.get(team) assert {:ok, _} = Removal.run(site) - assert Plausible.ConsolidatedView.enabled?(team) + assert Plausible.ConsolidatedView.get(team) end end end diff --git a/test/plausible/sites_test.exs b/test/plausible/sites_test.exs index fef7794db50e..b6aed3386f48 100644 --- a/test/plausible/sites_test.exs +++ b/test/plausible/sites_test.exs @@ -179,6 +179,7 @@ defmodule Plausible.SitesTest do test "resets consolidated view stats dates every time" do owner = new_user() new_site(owner: owner) + new_site(owner: owner) team = team_of(owner) consolidated_view = new_consolidated_view(team) diff --git a/test/plausible/stats/consolidated_view_sync_test.exs b/test/plausible/stats/consolidated_view_sync_test.exs deleted file mode 100644 index 5a50dfd98844..000000000000 --- a/test/plausible/stats/consolidated_view_sync_test.exs +++ /dev/null @@ -1,55 +0,0 @@ -defmodule Plausible.Stats.ConsolidatedViewSyncTest do - use Plausible.DataCase, async: true - - on_ee do - import Plausible.Teams.Test - import Plausible.ConsolidatedView, only: [ok_to_display?: 2, enable: 1] - - describe "ok_to_display?/2" do - setup [:create_user, :create_team] - - test "no user", %{team: team} do - refute ok_to_display?(team, nil) - end - - test "no team", %{user: user} do - refute ok_to_display?(nil, user) - end - - test "success", %{team: team, user: user} do - new_site(owner: user) - new_site(owner: user) - - team = Plausible.Teams.complete_setup(team) - - {:ok, _} = enable(team) - - patch_env(:super_admin_user_ids, [user.id]) - - assert ok_to_display?(team, user) - end - - test "not super-admin (temporary - feature-flag-like)", %{team: team, user: user} do - new_site(owner: user) - new_site(owner: user) - - team = Plausible.Teams.complete_setup(team) - - {:ok, _} = enable(team) - - refute ok_to_display?(team, user) - end - - test "not enabled", %{team: team, user: user} do - new_site(owner: user) - new_site(owner: user) - - team = Plausible.Teams.complete_setup(team) - - patch_env(:super_admin_user_ids, [user.id]) - - refute ok_to_display?(team, user) - end - end - end -end diff --git a/test/plausible/stats/consolidated_view_test.exs b/test/plausible/stats/consolidated_view_test.exs index 5ccce28b6046..316bb2bd7a6d 100644 --- a/test/plausible/stats/consolidated_view_test.exs +++ b/test/plausible/stats/consolidated_view_test.exs @@ -72,6 +72,7 @@ defmodule Plausible.Stats.ConsolidatedViewTest do fixed_now = ~N[2025-10-20 12:49:15] owner = new_user() site = new_site(owner: owner) + new_site(owner: owner) populate_stats(site, [ build(:pageview, user_id: 111, timestamp: ~N[2025-10-20 12:00:00]), @@ -95,6 +96,7 @@ defmodule Plausible.Stats.ConsolidatedViewTest do fixed_now = ~N[2025-10-20 12:49:15] owner = new_user() site = new_site(owner: owner) + new_site(owner: owner) populate_stats(site, [ build(:pageview, user_id: 111, timestamp: ~N[2025-10-19 11:00:00]), @@ -122,6 +124,7 @@ defmodule Plausible.Stats.ConsolidatedViewTest do fixed_now = ~N[2025-10-20 12:49:15] owner = new_user() site = new_site(owner: owner) + new_site(owner: owner) populate_stats(site, [ build(:pageview, user_id: 111, timestamp: ~N[2025-10-20 12:00:00]), diff --git a/test/plausible/stats/query_parser_test.exs b/test/plausible/stats/query_parser_test.exs index f3187c13a481..ab4eb5b4d809 100644 --- a/test/plausible/stats/query_parser_test.exs +++ b/test/plausible/stats/query_parser_test.exs @@ -2718,6 +2718,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do end test "is set to a list of site_ids when site is consolidated", %{site: site} do + new_site(team: site.team) cv = new_consolidated_view(site.team) params = %{ @@ -2726,10 +2727,13 @@ defmodule Plausible.Stats.Filters.QueryParserTest do "date_range" => "all" } - site_id = site.id + assert {:ok, %{consolidated_site_ids: site_ids}} = parse(cv, :public, params) + assert length(site_ids) == 2 + assert site.id in site_ids - assert {:ok, %{consolidated_site_ids: [^site_id]}} = parse(cv, :public, params) - assert {:ok, %{consolidated_site_ids: [^site_id]}} = parse(cv, :internal, params) + assert {:ok, %{consolidated_site_ids: site_ids}} = parse(cv, :internal, params) + assert length(site_ids) == 2 + assert site.id in site_ids end end end diff --git a/test/plausible/stats/query_test.exs b/test/plausible/stats/query_test.exs index 6d8252c0c6aa..d1fd76fb5b10 100644 --- a/test/plausible/stats/query_test.exs +++ b/test/plausible/stats/query_test.exs @@ -492,10 +492,12 @@ defmodule Plausible.Stats.QueryTest do end test "is set to a list of site_ids when site is consolidated", %{site: site} do + new_site(team: site.team) cv = new_consolidated_view(site.team) - site_id = site.id - assert %{consolidated_site_ids: [^site_id]} = Query.from(cv, %{"period" => "day"}) + assert %{consolidated_site_ids: site_ids} = Query.from(cv, %{"period" => "day"}) + assert length(site_ids) == 2 + assert site.id in site_ids end end end diff --git a/test/plausible/teams/sites/transfer_test.exs b/test/plausible/teams/sites/transfer_test.exs index 620852624ff0..389beecb7a92 100644 --- a/test/plausible/teams/sites/transfer_test.exs +++ b/test/plausible/teams/sites/transfer_test.exs @@ -90,10 +90,11 @@ defmodule Plausible.Teams.Sites.TransferTest do test "disables consolidated view if sites transferred out of team" do user = new_user() site = new_site(owner: user) + new_site(owner: user) team = team_of(user) new_consolidated_view(team) - assert ConsolidatedView.enabled?(team) + assert ConsolidatedView.get(team) another_owner = new_user() subscribe_to_growth_plan(another_owner) @@ -104,7 +105,7 @@ defmodule Plausible.Teams.Sites.TransferTest do :ok = Transfer.change_team(site, user, another_team) - refute ConsolidatedView.enabled?(team) + refute ConsolidatedView.get(team) end end diff --git a/test/plausible_web/controllers/api/external_sites_controller_sites_crud_api_test.exs b/test/plausible_web/controllers/api/external_sites_controller_sites_crud_api_test.exs index 785ddfccf254..79db3fee07d1 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_sites_crud_api_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_sites_crud_api_test.exs @@ -757,7 +757,8 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do subscribe_to_enterprise_plan(user, features: [ Plausible.Billing.Feature.StatsAPI, - Plausible.Billing.Feature.SitesAPI + Plausible.Billing.Feature.SitesAPI, + Plausible.Billing.Feature.ConsolidatedView ] ) diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index 5555caee4568..aa28867508d1 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -504,7 +504,9 @@ defmodule PlausibleWeb.SiteControllerTest do on_ee do test "renders only timezone section for a consolidated site", %{conn: conn, user: user} do - consolidated_view = user |> team_of() |> new_consolidated_view() + team = team_of(user) + new_site(team: team) + consolidated_view = new_consolidated_view(team) conn = get(conn, "/#{consolidated_view.domain}/settings/general") resp = html_response(conn, 200) @@ -584,6 +586,7 @@ defmodule PlausibleWeb.SiteControllerTest do user: user } do team = user |> team_of() + new_site(team: team) site = new_consolidated_view(team) conn = get(conn, "/#{site.domain}/settings/general") resp = html_response(conn, 200) diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index a2e6783aeb28..618b1419c909 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -30,7 +30,7 @@ defmodule PlausibleWeb.StatsControllerTest do assert text_of_attr(resp, @react_container, "data-current-user-id") == "null" assert text_of_attr(resp, @react_container, "data-embedded") == "" assert text_of_attr(resp, @react_container, "data-is-consolidated-view") == "false" - assert text_of_attr(resp, @react_container, "data-team-has-consolidated-view") == "false" + assert text_of_attr(resp, @react_container, "data-consolidated-view-available") == "false" assert text_of_attr(resp, @react_container, "data-team-identifier") == site.team.identifier assert "noindex, nofollow" == @@ -185,6 +185,8 @@ defmodule PlausibleWeb.StatsControllerTest do conn: conn, user: user } do + new_site(owner: user) + new_site(owner: user) cv = user |> team_of() |> new_consolidated_view() conn = get(conn, "/" <> cv.domain) @@ -195,6 +197,23 @@ defmodule PlausibleWeb.StatsControllerTest do assert text_of_attr(resp, @react_container, "data-current-user-role") == "owner" assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" end + + test "redirects to /sites if for some reason ineligible anymore", %{ + conn: conn, + user: user + } do + new_site(owner: user) + new_site(owner: user) + cv = user |> team_of() |> new_consolidated_view() + + user + |> team_of() + |> Plausible.Teams.Team.end_trial() + |> Plausible.Repo.update!() + + conn = get(conn, "/" <> cv.domain) + assert redirected_to(conn, 302) == "/sites" + end end @tag :ee_only diff --git a/test/plausible_web/live/customer_support/teams_test.exs b/test/plausible_web/live/customer_support/teams_test.exs index 7124dde6f382..10212a1eb3ea 100644 --- a/test/plausible_web/live/customer_support/teams_test.exs +++ b/test/plausible_web/live/customer_support/teams_test.exs @@ -16,13 +16,13 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do Routes.customer_support_team_path(PlausibleWeb.Endpoint, :show, id, qs) end - describe "overview" do - setup [:create_user, :log_in, :create_site] + setup [:create_user, :log_in, :create_site] - setup %{user: user} do - patch_env(:super_admin_user_ids, [user.id]) - end + setup %{user: user} do + patch_env(:super_admin_user_ids, [user.id]) + end + describe "overview" do test "renders", %{conn: conn, user: user} do team = team_of(user) new_site(owner: user) @@ -165,12 +165,6 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do end describe "sites" do - setup [:create_user, :log_in, :create_site] - - setup %{user: user} do - patch_env(:super_admin_user_ids, [user.id]) - end - test "lists sites belonging to a team", %{conn: conn, user: user} do team = team_of(user) new_site(owner: user, domain: "primary.example.com/test") @@ -198,13 +192,7 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do @consolidated_views_tab_content ~s|div[data-test-id="consolidated-views-tab-content"]| describe "consolidated views" do - setup [:create_user, :log_in, :create_site] - - setup %{user: user} do - patch_env(:super_admin_user_ids, [user.id]) - end - - test "renders button to create one inonef exist yet", %{conn: conn, user: user} do + test "renders button to create one if none exist yet", %{conn: conn, user: user} do team = team_of(user) {:ok, lv, _html} = live(conn, open_team(team.id, tab: "consolidated_views")) @@ -217,16 +205,19 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do end test "can create a consolidated view for team", %{conn: conn, user: user} do + new_site(owner: user) + new_site(owner: user) team = user |> team_of() |> Plausible.Teams.complete_setup() {:ok, lv, _html} = live(conn, open_team(team.id, tab: "consolidated_views")) lv |> element(@create_consolidated_view_button) |> render_click() - assert Plausible.ConsolidatedView.enabled?(team) + assert Plausible.ConsolidatedView.get(team) end test "renders existing consolidated view", %{conn: conn, user: user} do + new_site(owner: user) team = team_of(user) new_consolidated_view(team) @@ -242,24 +233,21 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do end test "can delete consolidated view", %{conn: conn, user: user} do + new_site(owner: user) team = team_of(user) new_consolidated_view(team) + assert Plausible.ConsolidatedView.get(team) + {:ok, lv, _html} = live(conn, open_team(team.id, tab: "consolidated_views")) lv |> element(@delete_consolidated_view_button) |> render_click() - assert not Plausible.ConsolidatedView.enabled?(team) + refute Plausible.ConsolidatedView.get(team) end end describe "billing" do - setup [:create_user, :log_in, :create_site] - - setup %{user: user} do - patch_env(:super_admin_user_ids, [user.id]) - end - test "renders custom plan form", %{conn: conn, user: user} do lv = open_custom_plan(conn, team_of(user)) html = render(lv) @@ -327,7 +315,8 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do "site_segments" => "false", "shared_links" => "true", "sites_api" => "true", - "sso" => "false" + "sso" => "false", + "consolidated_view" => "true" } } }) @@ -342,6 +331,7 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do %Plausible.Billing.EnterprisePlan{ billing_interval: :yearly, features: [ + Plausible.Billing.Feature.ConsolidatedView, Plausible.Billing.Feature.SharedLinks, Plausible.Billing.Feature.SitesAPI ], @@ -745,12 +735,6 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do end describe "sso" do - setup [:create_user, :log_in, :create_site] - - setup %{user: user} do - patch_env(:super_admin_user_ids, [user.id]) - end - test "sso tab normally won't render", %{conn: conn, user: user} do team = team_of(user) {:ok, _lv, html} = live(conn, open_team(team.id)) @@ -840,12 +824,6 @@ defmodule PlausibleWeb.Live.CustomerSupport.TeamsTest do end describe "audit" do - setup [:create_user, :log_in, :create_site] - - setup %{user: user} do - patch_env(:super_admin_user_ids, [user.id]) - end - test "audit tab is present", %{conn: conn, user: user} do team = team_of(user) {:ok, _lv, html} = live(conn, open_team(team.id)) diff --git a/test/plausible_web/live/goal_settings/form_test.exs b/test/plausible_web/live/goal_settings/form_test.exs index 1910bb54d528..ef6d2b6afdb0 100644 --- a/test/plausible_web/live/goal_settings/form_test.exs +++ b/test/plausible_web/live/goal_settings/form_test.exs @@ -135,6 +135,7 @@ defmodule PlausibleWeb.Live.GoalSettings.FormTest do user: user } do {:ok, team} = Plausible.Teams.get_or_create(user) + new_site(team: team) site = new_consolidated_view(team) lv = get_liveview(conn, site) diff --git a/test/plausible_web/live/sites_test.exs b/test/plausible_web/live/sites_test.exs index 3b0801c61b63..bd073cffaede 100644 --- a/test/plausible_web/live/sites_test.exs +++ b/test/plausible_web/live/sites_test.exs @@ -283,14 +283,10 @@ defmodule PlausibleWeb.Live.SitesTest do on_ee do describe "consolidated views appearance" do - setup %{user: user} do - # this is temporary, instead of feature flag we'll only show consolidated views to super admins - patch_env(:super_admin_user_ids, [user.id]) - end - test "consolidated view shows up", %{conn: conn, user: user} do new_site(owner: user) - team = team_of(user) + new_site(owner: user) + team = user |> team_of() conn = set_current_team(conn, team) @@ -298,7 +294,8 @@ defmodule PlausibleWeb.Live.SitesTest do refute element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) - new_consolidated_view(team) + team = Plausible.Teams.complete_setup(team) + conn = set_current_team(conn, team) {:ok, _lv, html} = live(conn, "/sites") @@ -321,12 +318,10 @@ defmodule PlausibleWeb.Live.SitesTest do build(:pageview, user_id: 3) ]) - team = team_of(user) + team = user |> team_of() |> Plausible.Teams.complete_setup() conn = set_current_team(conn, team) - new_consolidated_view(team) - {:ok, _lv, html} = live(conn, "/sites") stats = text_of_element(html, ~s|[data-test-id="consolidated-view-stats-loaded"]|) @@ -336,24 +331,219 @@ defmodule PlausibleWeb.Live.SitesTest do assert stats =~ "Views per visit 1.33" end - test "consolidated view does not show up for non-superadmin (temp)", %{conn: conn} do - user = new_user() + test "consolidated view does not show up when flag is down (temp) during trial", %{ + conn: conn, + user: user + } do + new_site(owner: user) new_site(owner: user) - team = team_of(user) + + team = user |> team_of() |> Plausible.Teams.complete_setup() + + FunWithFlags.disable(:consolidated_view, for_actor: team) conn = set_current_team(conn, team) {:ok, _lv, html} = live(conn, "/sites") refute element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) + refute element_exists?(html, ~s|[data-test-id="consolidated-view-stats-loaded"]|) + refute element_exists?(html, ~s|[data-test-id="consolidated-view-chart-loaded"]|) + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + end + + test "consolidated view does not show up when flag is down (temp) after trial ends", %{ + conn: conn, + user: user + } do + new_site(owner: user) + new_site(owner: user) - new_consolidated_view(team) + team = + user + |> team_of() + |> Plausible.Teams.Team.end_trial() + |> Plausible.Repo.update!() + + FunWithFlags.disable(:consolidated_view, for_actor: team) + + conn = set_current_team(conn, team) {:ok, _lv, html} = live(conn, "/sites") refute element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) refute element_exists?(html, ~s|[data-test-id="consolidated-view-stats-loaded"]|) refute element_exists?(html, ~s|[data-test-id="consolidated-view-chart-loaded"]|) + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + end + + test "consolidated view disappears when trial ends - CTA is shown instead", %{ + conn: conn, + user: user + } do + new_site(owner: user) + new_site(owner: user) + team = user |> team_of() |> Plausible.Teams.complete_setup() + + conn = set_current_team(conn, team) + + {:ok, _lv, html} = live(conn, "/sites") + + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + assert element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) + + team |> Plausible.Teams.Team.end_trial() |> Plausible.Repo.update!() + + {:ok, _lv, html} = live(conn, "/sites") + + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) + assert element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + + assert text_of_element(html, ~s|[data-test-id="consolidated-view-card-cta"]|) =~ + "Upgrade to the Business plan to enable consolidated views." + + assert element_exists?( + html, + ~s|[data-test-id="consolidated-view-card-cta"] a[href$="/billing/choose-plan"]| + ) + end + + test "a team that hasn't been set up shows different CTA", %{ + conn: conn, + user: user + } do + new_site(owner: user) + new_site(owner: user) + + {:ok, _lv, html} = live(conn, "/sites") + + assert element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + + assert text_of_element(html, ~s|[data-test-id="consolidated-view-card-cta"]|) =~ + "To create a consolidated view, you'll need to set up a team." + end + + test "single site won't show neither CTA or view - team not setup", %{ + conn: conn, + user: user + } do + new_site(owner: user) + + {:ok, _lv, html} = live(conn, "/sites") + + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) + end + + test "single site won't show neither CTA or view - team setup", %{ + conn: conn, + user: user + } do + new_site(owner: user) + + user |> team_of() |> Plausible.Teams.complete_setup() + + {:ok, _lv, html} = live(conn, "/sites") + + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) + end + + test "CTA advertises contacting team owner to viewers", %{ + conn: conn, + user: user + } do + new_site(owner: user) + new_site(owner: user) + + subscribe_to_growth_plan(user) + + team = user |> team_of() |> Plausible.Teams.complete_setup() + + viewer = add_member(team, role: :viewer) + + {:ok, conn: conn} = log_in(%{user: viewer, conn: conn}) + + {:ok, _lv, html} = live(conn, "/sites?__team=#{team.identifier}") + + assert element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + + assert text_of_element(html, ~s|[data-test-id="consolidated-view-card-cta"]|) =~ + "Available on Business plans. Contact your team owner to create it." + + refute element_exists?( + html, + ~s|[data-test-id="consolidated-view-card-cta"] a[href="/billing/choose-plan"]| + ) + end + + test "CTA can be permanently dismissed, in which case dropdown option to restore it shows up", + %{conn: conn, user: user} do + new_site(owner: user) + new_site(owner: user) + + dismiss_selector = ~s|[phx-click="consolidated-view-cta-dismiss"]| + cta_selector = ~s|[data-test-id="consolidated-view-card-cta"]| + restore_selector = ~s|[phx-click="consolidated-view-cta-restore"]| + + subscribe_to_growth_plan(user) + + {:ok, lv, html} = live(conn, "/sites") + + assert element_exists?(html, cta_selector) + refute element_exists?(html, restore_selector) + + lv + |> element(dismiss_selector) + |> render_click() + + html = render(lv) + + refute element_exists?(html, cta_selector) + assert element_exists?(html, restore_selector) + + {:ok, _lv, html} = live(conn, "/sites") + refute element_exists?(html, cta_selector) + + lv + |> element(restore_selector) + |> render_click() + + html = render(lv) + assert element_exists?(html, cta_selector) + end + + test "consolidated view card disappears when searching", %{conn: conn, user: user} do + new_site(owner: user) + new_site(owner: user) + + team = user |> team_of() |> Plausible.Teams.complete_setup() + conn = set_current_team(conn, team) + + {:ok, lv, html} = live(conn, "/sites") + + assert element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) + + type_into_input(lv, "filter-text", "a") + + html = render(lv) + + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card"]|) + end + + test "CTA card disappears when searching", %{conn: conn, user: user} do + new_site(owner: user) + new_site(owner: user) + + {:ok, lv, html} = live(conn, "/sites") + + assert element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) + + type_into_input(lv, "filter-text", "a") + + html = render(lv) + + refute element_exists?(html, ~s|[data-test-id="consolidated-view-card-cta"]|) end end end diff --git a/test/plausible_web/plugins/api/controllers/capabilities_test.exs b/test/plausible_web/plugins/api/controllers/capabilities_test.exs index ee1efb19927d..39eec985defd 100644 --- a/test/plausible_web/plugins/api/controllers/capabilities_test.exs +++ b/test/plausible_web/plugins/api/controllers/capabilities_test.exs @@ -33,7 +33,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do "SitesAPI" => false, "SiteSegments" => false, "SharedLinks" => false, - "SSO" => false + "SSO" => false, + "ConsolidatedView" => false } } @@ -61,7 +62,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do "SitesAPI" => false, "SiteSegments" => false, "SharedLinks" => false, - "SSO" => false + "SSO" => false, + "ConsolidatedView" => false } } @@ -91,7 +93,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do "SitesAPI" => false, "SiteSegments" => true, "SharedLinks" => true, - "SSO" => false + "SSO" => false, + "ConsolidatedView" => true } } @@ -123,7 +126,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do "SitesAPI" => false, "SiteSegments" => false, "SharedLinks" => true, - "SSO" => false + "SSO" => false, + "ConsolidatedView" => false } } @@ -158,7 +162,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do "SitesAPI" => true, "SiteSegments" => false, "SharedLinks" => false, - "SSO" => false + "SSO" => false, + "ConsolidatedView" => false } } diff --git a/test/test_helper.exs b/test/test_helper.exs index 8f7a58d08cba..60dcf4286664 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -11,8 +11,7 @@ Mox.defmock(Plausible.DnsLookup.Mock, Application.ensure_all_started(:double) -FunWithFlags.enable(:channels) -FunWithFlags.enable(:scroll_depth) +FunWithFlags.enable(:consolidated_view) Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, :manual)