diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex index 98b0ffb709f1..7754a05c2750 100644 --- a/lib/plausible/sites.ex +++ b/lib/plausible/sites.ex @@ -136,9 +136,6 @@ defmodule Plausible.Sites do defdelegate list(user, pagination_params, opts \\ []), to: Plausible.Teams.Sites - defdelegate list_with_invitations(user, pagination_params, opts \\ []), - to: Plausible.Teams.Sites - def list_people(site) do owner_memberships = from( diff --git a/lib/plausible/teams/sites.ex b/lib/plausible/teams/sites.ex index 552b3ceca3e0..1937a3f06c09 100644 --- a/lib/plausible/teams/sites.ex +++ b/lib/plausible/teams/sites.ex @@ -1,19 +1,21 @@ defmodule Plausible.Teams.Sites do @moduledoc false + @sample_threshold 10_000_000 import Ecto.Query + use Plausible.Stats.SQL.Fragments alias Plausible.Auth alias Plausible.Repo alias Plausible.Site alias Plausible.Teams - @type list_opt() :: {:filter_by_domain, String.t()} | {:team, Teams.Team.t() | nil} + def list_with_clickhouse(user, team, opts \\ []) do + # TODO maybe filter by domain + date_range = Keyword.get(opts, :date_range, {:last_n_days, 30}) + now = Keyword.get(opts, :now, DateTime.utc_now()) - @spec list(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t() - def list(user, pagination_params, opts \\ []) do - domain_filter = Keyword.get(opts, :filter_by_domain) - team = Keyword.get(opts, :team) + {relative_start_date, relative_end_date} = calculate_relative_dates(date_range, now) all_query = if Teams.setup?(team) do @@ -22,90 +24,204 @@ defmodule Plausible.Teams.Sites do inner_join: s in assoc(t, :sites), where: tm.user_id == ^user.id and tm.role != :guest, where: tm.team_id == ^team.id, - select: %{site_id: s.id, entry_type: "site"} + where: not s.consolidated, + select: struct(s, [:id, :domain, :timezone]) ) else my_team_query = from(tm in Teams.Membership, inner_join: t in assoc(tm, :team), inner_join: s in assoc(t, :sites), + where: not s.consolidated, where: tm.user_id == ^user.id and tm.role != :guest, - where: tm.is_autocreated == true, - where: t.setup_complete == false, - select: %{site_id: s.id, entry_type: "site"} + where: tm.is_autocreated, + where: not t.setup_complete, + select: struct(s, [:id, :domain, :timezone]) ) guest_membership_query = - from tm in Teams.Membership, + from(tm in Teams.Membership, inner_join: gm in assoc(tm, :guest_memberships), inner_join: s in assoc(gm, :site), + where: not s.consolidated, where: tm.user_id == ^user.id and tm.role == :guest, - select: %{site_id: s.id, entry_type: "site"} + select: struct(s, [:id, :domain, :timezone]) + ) - from s in my_team_query, + from(s in my_team_query, union_all: ^guest_membership_query + ) end - from(u in subquery(all_query), - inner_join: s in ^Plausible.Site.regular(), - on: u.site_id == s.id, - as: :site, - left_join: up in Site.UserPreference, - on: up.site_id == s.id and up.user_id == ^user.id, - select: %{ - s - | entry_type: - selected_as( - fragment( - """ - CASE - WHEN ? IS NOT NULL THEN 'pinned_site' - ELSE ? - END - """, - up.pinned_at, - u.entry_type + all_query = + from(u in subquery(all_query), + inner_join: s in ^Plausible.Site.regular(), + on: u.id == s.id, + as: :site, + left_join: up in Site.UserPreference, + on: up.site_id == s.id and up.user_id == ^user.id, + select: %{ + s + | entry_type: + selected_as( + fragment( + """ + CASE + WHEN ? IS NOT NULL THEN 'pinned_site' + ELSE ? + END + """, + up.pinned_at, + "site" + ), + :entry_type ), - :entry_type + pinned_at: selected_as(up.pinned_at, :pinned_at) + } + ) + + clickhouse_query = + from(e in Plausible.ClickhouseEventV2, + hints: unsafe_fragment(^"SAMPLE #{@sample_threshold}"), + right_join: sites in subquery(all_query, prefix: "postgres_remote"), + on: fragment("CAST(?, 'UInt64')", sites.id) == e.site_id, + select: %{ + entry_type: selected_as(sites.entry_type, :entry_type), + pinned_at: selected_as(sites.pinned_at, :pinned_at), + site_id: sites.id, + domain: sites.domain, + timezone: sites.timezone, + visitors: + selected_as( + scale_sample(fragment("uniqIf(?, ? != 0)", e.user_id, e.site_id)), + :visitors + ) + }, + where: + e.site_id == 0 or + fragment( + """ + toString(?, ?) >= concat(?, ' 00:00:00') + AND toString(?, ?) <= concat(?, ' 23:59:59') + """, + e.timestamp, + sites.timezone, + ^relative_start_date, + e.timestamp, + sites.timezone, + ^relative_end_date ), - pinned_at: selected_as(up.pinned_at, :pinned_at) - }, - order_by: [ - asc: selected_as(:entry_type), - desc: selected_as(:pinned_at), - asc: s.domain - ] + group_by: [sites.id, sites.domain, sites.entry_type, sites.pinned_at, sites.timezone], + order_by: [ + asc: selected_as(:entry_type), + desc: selected_as(:pinned_at), + desc: selected_as(:visitors) + ] + ) + + clickhouse_query |> dbg() + + Paginator.paginate( + clickhouse_query, + [limit: 24, cursor_fields: [:visitors, :id], sort_direction: :desc], + Plausible.ClickhouseRepo, + [] ) - |> maybe_filter_by_domain(domain_filter) - |> Repo.paginate(pagination_params) end + # Helper function to calculate date range for ClickHouse queries + # Returns {start_date, end_date} as Date structs or date strings + defp calculate_relative_dates({:last_n_days, n}, now) do + end_date = now |> DateTime.to_date() |> Date.add(-1) + start_date = end_date |> Date.add(-(n - 1)) + {Date.to_string(start_date), Date.to_string(end_date)} + end + + defp calculate_relative_dates(:day, now) do + date = DateTime.to_date(now) + {Date.to_string(date), Date.to_string(date)} + end + + defp calculate_relative_dates(:month, now) do + date = DateTime.to_date(now) + start_date = Date.beginning_of_month(date) + end_date = Date.end_of_month(date) + {Date.to_string(start_date), Date.to_string(end_date)} + end + + defp calculate_relative_dates(:year, now) do + date = DateTime.to_date(now) + start_date = %{date | month: 1, day: 1} + end_date = %{date | month: 12, day: 31} + {Date.to_string(start_date), Date.to_string(end_date)} + end + + defp calculate_relative_dates({:date_range, from, to}, _now) do + {Date.to_string(from), Date.to_string(to)} + end + + @type list_opt() :: {:filter_by_domain, String.t()} | {:team, Teams.Team.t() | nil} + @role_type Plausible.Teams.Invitation.__schema__(:type, :role) - @spec list_with_invitations(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t() - def list_with_invitations(user, pagination_params, opts \\ []) do + @spec list(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t() + def list(user, pagination_params, opts \\ []) do domain_filter = Keyword.get(opts, :filter_by_domain) team = Keyword.get(opts, :team) - union_query = + all_query = if Teams.setup?(team) do - list_with_invitations_setup_query(team, user) + from(tm in Teams.Membership, + inner_join: t in assoc(tm, :team), + inner_join: s in assoc(t, :sites), + where: tm.user_id == ^user.id and tm.role != :guest, + where: tm.team_id == ^team.id, + select: %{site_id: s.id, entry_type: "site", role: tm.role} + ) else - list_with_invitations_personal_query(team, user) + my_team_query = + from(tm in Teams.Membership, + inner_join: t in assoc(tm, :team), + inner_join: s in assoc(t, :sites), + where: tm.user_id == ^user.id and tm.role != :guest, + where: tm.is_autocreated == true, + where: not t.setup_complete, + select: %{site_id: s.id, entry_type: "site", role: tm.role} + ) + + guest_membership_query = + from(tm in Teams.Membership, + inner_join: gm in assoc(tm, :guest_memberships), + inner_join: s in assoc(gm, :site), + where: tm.user_id == ^user.id and tm.role == :guest, + select: %{ + site_id: s.id, + entry_type: "site", + role: + fragment( + """ + CASE + WHEN ? = 'editor' THEN 'admin' + ELSE ? + END + """, + gm.role, + gm.role + ) + } + ) + + from(s in my_team_query, + union_all: ^guest_membership_query + ) end - from(u in subquery(union_query), + from(u in subquery(all_query), inner_join: s in ^Plausible.Site.regular(), on: u.site_id == s.id, as: :site, left_join: up in Site.UserPreference, on: up.site_id == s.id and up.user_id == ^user.id, - left_join: ti in Teams.Invitation, - on: ti.id == u.team_invitation_id, - left_join: gi in Teams.GuestInvitation, - on: gi.id == u.guest_invitation_id, - left_join: st in Teams.SiteTransfer, - on: st.id == u.transfer_id, select: %{ s | entry_type: @@ -113,14 +229,10 @@ defmodule Plausible.Teams.Sites do fragment( """ CASE - WHEN ? IS NOT NULL THEN 'invitation' - WHEN ? IS NOT NULL THEN 'invitation' WHEN ? IS NOT NULL THEN 'pinned_site' ELSE ? END """, - gi.id, - st.id, up.pinned_at, u.entry_type ), @@ -133,15 +245,6 @@ defmodule Plausible.Teams.Sites do site_id: s.id, site: s } - ], - invitations: [ - %{ - invitation_id: coalesce(gi.invitation_id, st.transfer_id), - email: coalesce(ti.email, st.email), - role: type(u.role, ^@role_type), - site_id: s.id, - site: s - } ] }, order_by: [ @@ -152,177 +255,6 @@ defmodule Plausible.Teams.Sites do ) |> maybe_filter_by_domain(domain_filter) |> Repo.paginate(pagination_params) - |> Map.update!(:entries, fn entries -> - Enum.map(entries, fn - %{invitation: [%{invitation_id: nil}]} = entry -> - %{entry | invitations: []} - - entry -> - entry - end) - end) - end - - defp list_with_invitations_setup_query(team, user) do - team_membership_query = - from(tm in Teams.Membership, - inner_join: t in assoc(tm, :team), - inner_join: u in assoc(tm, :user), - as: :user, - inner_join: s in assoc(t, :sites), - as: :site, - where: tm.user_id == ^user.id and tm.role != :guest, - where: tm.team_id == ^team.id, - select: %{ - site_id: s.id, - entry_type: "site", - guest_invitation_id: 0, - team_invitation_id: 0, - role: tm.role, - transfer_id: 0 - } - ) - - site_transfer_query = - from st in Teams.SiteTransfer, - as: :site_transfer, - inner_join: s in assoc(st, :site), - as: :site, - where: s.team_id != ^team.id, - where: st.email == ^user.email, - where: - exists( - from tm in Teams.Membership, - inner_join: u in assoc(tm, :user), - where: tm.team_id == ^team.id, - where: u.email == parent_as(:site_transfer).email, - where: tm.role in [:owner, :admin], - select: 1 - ), - select: %{ - site_id: s.id, - entry_type: "invitation", - guest_invitation_id: 0, - team_invitation_id: 0, - role: "owner", - transfer_id: st.id - } - - from s in team_membership_query, - union_all: ^site_transfer_query - end - - defp list_with_invitations_personal_query(team, user) do - my_team_query = - from(tm in Teams.Membership, - inner_join: t in assoc(tm, :team), - inner_join: u in assoc(tm, :user), - as: :user, - inner_join: s in assoc(t, :sites), - as: :site, - where: tm.user_id == ^user.id and tm.role == :owner, - where: t.setup_complete == false, - select: %{ - site_id: s.id, - entry_type: "site", - guest_invitation_id: 0, - team_invitation_id: 0, - role: tm.role, - transfer_id: 0 - } - ) - - guest_membership_query = - from(tm in Teams.Membership, - inner_join: u in assoc(tm, :user), - as: :user, - inner_join: gm in assoc(tm, :guest_memberships), - inner_join: s in assoc(gm, :site), - as: :site, - where: tm.user_id == ^user.id and tm.role == :guest, - select: %{ - site_id: s.id, - entry_type: "site", - guest_invitation_id: 0, - team_invitation_id: 0, - role: - fragment( - """ - CASE - WHEN ? = 'editor' THEN 'admin' - ELSE ? - END - """, - gm.role, - gm.role - ), - transfer_id: 0 - } - ) - - guest_invitation_query = - from ti in Teams.Invitation, - as: :team_invitation, - inner_join: gi in assoc(ti, :guest_invitations), - inner_join: s in assoc(gi, :site), - as: :site, - where: - not exists( - from tm in Teams.Membership, - inner_join: u in assoc(tm, :user), - left_join: gm in assoc(tm, :guest_memberships), - on: gm.site_id == parent_as(:site).id, - where: tm.team_id == parent_as(:team_invitation).team_id, - where: u.email == parent_as(:team_invitation).email, - where: not is_nil(gm.id) or tm.role != :guest, - select: 1 - ), - where: ti.email == ^user.email and ti.role == :guest, - select: %{ - site_id: s.id, - entry_type: "invitation", - guest_invitation_id: gi.id, - team_invitation_id: ti.id, - role: - fragment( - """ - CASE - WHEN ? = 'editor' THEN 'admin' - ELSE ? - END - """, - gi.role, - gi.role - ), - transfer_id: 0 - } - - site_transfer_query = - from st in Teams.SiteTransfer, - as: :site_transfer, - inner_join: s in assoc(st, :site), - as: :site, - where: st.email == ^user.email, - select: %{ - site_id: s.id, - entry_type: "invitation", - guest_invitation_id: 0, - team_invitation_id: 0, - role: "owner", - transfer_id: st.id - } - - site_transfer_query = - if team do - where(site_transfer_query, [site: s], s.team_id != ^team.id) - else - site_transfer_query - end - - from s in my_team_query, - union_all: ^guest_membership_query, - union_all: ^guest_invitation_query, - union_all: ^site_transfer_query end defp maybe_filter_by_domain(query, domain) diff --git a/lib/plausible_web/live/settings/cancel_flow.ex b/lib/plausible_web/live/settings/cancel_flow.ex new file mode 100644 index 000000000000..bde1021cac2a --- /dev/null +++ b/lib/plausible_web/live/settings/cancel_flow.ex @@ -0,0 +1,18 @@ +defmodule PlausibleWeb.Live.CancelFlow do + @moduledoc """ + Live view for subscription cancel flow + """ + use PlausibleWeb, :live_view + + def mount(_params, _session, socket) do + {:ok, socket} + end + + def render(assigns) do + ~H""" +
+ Cancel +
+ """ + end +end diff --git a/lib/plausible_web/live/sites.ex b/lib/plausible_web/live/sites.ex index 8c71fd335140..d3cc16e5de71 100644 --- a/lib/plausible_web/live/sites.ex +++ b/lib/plausible_web/live/sites.ex @@ -24,10 +24,6 @@ defmodule PlausibleWeb.Live.Sites do socket = socket |> assign(:uri, uri) - |> assign( - :team_invitations, - Teams.Invitations.all(user) - ) |> assign(:hourly_stats, %{}) |> assign(:filter_text, String.trim(params["filter_text"] || "")) |> assign(init_consolidated_view_assigns(user, team)) @@ -84,13 +80,7 @@ defmodule PlausibleWeb.Live.Sites do end def render(assigns) do - assigns = - assign( - assigns, - :invitations_map, - Enum.map(assigns.invitations, &{&1.invitation.invitation_id, &1}) |> Enum.into(%{}) - ) - |> assign(:searching?, String.trim(assigns.filter_text) != "") + assigns = assign(assigns, :searching?, String.trim(assigns.filter_text) != "") ~H""" <.flash_messages flash={@flash} /> @@ -114,8 +104,6 @@ defmodule PlausibleWeb.Live.Sites do - -
<%= for site <- @sites.entries do %> <.site - :if={site.entry_type in ["pinned_site", "site"]} site={site} hourly_stats={Map.get(@hourly_stats, site.domain, :loading)} /> - <.invitation - :if={site.entry_type == "invitation"} - site={site} - invitation={@invitations_map[hd(site.invitations).invitation_id]} - hourly_stats={Map.get(@hourly_stats, site.domain, :loading)} - /> <% end %> @@ -484,41 +465,6 @@ defmodule PlausibleWeb.Live.Sites do """ end - attr(:site, Plausible.Site, required: true) - attr(:invitation, :map, required: true) - attr(:hourly_stats, :map, required: true) - - def invitation(assigns) do - assigns = - assigns - |> assign(:modal_id, "invitation-modal-#{assigns[:invitation].invitation.invitation_id}") - - ~H""" -
  • -
    -
    - <.favicon domain={@site.domain} /> -
    -

    - {@site.domain} -

    -
    - <.pill color={:green}> - Pending invitation - -
    - <.site_stats hourly_stats={@hourly_stats} /> -
    - <.invitation_modal id={@modal_id} site={@site} invitation={@invitation} /> -
  • - """ - end - attr(:site, Plausible.Site, required: true) attr(:hourly_stats, :map, required: true) @@ -720,117 +666,6 @@ defmodule PlausibleWeb.Live.Sites do """ end - attr(:id, :string, required: true) - attr(:site, Plausible.Site, required: true) - attr(:invitation, :map, required: true) - - def invitation_modal(assigns) do - ~H""" - -
    - -
    - - You're invited to {@site.domain} - -
    -

    - You've been added as {@invitation.invitation.role} - to the {@site.domain} analytics dashboard. - <%= if !(Map.get(@invitation, :exceeded_limits) || Map.get(@invitation, :no_plan)) && - @invitation.invitation.role == :owner do %> - If you accept the ownership transfer, you will be responsible for billing going forward. - <% else %> - Welcome aboard! - <% end %> -

    -
    -
    - <.notice - :if={Map.get(@invitation, :missing_features)} - title="Missing features" - class="mt-4 shadow-xs dark:shadow-none" - > -

    - The site uses {Map.get(@invitation, :missing_features)}, - which your current subscription does not support. After accepting ownership of this site, - you will not be able to access them unless you <.styled_link - class="inline-block" - href={Routes.billing_path(PlausibleWeb.Endpoint, :choose_plan)} - > - upgrade to a suitable plan - . -

    - - <.notice - :if={Map.get(@invitation, :exceeded_limits)} - title="Unable to accept site ownership" - class="mt-4 shadow-xs dark:shadow-none" - > -

    - Owning this site would exceed your {Map.get(@invitation, :exceeded_limits)}. Please check your usage in - <.styled_link - class="inline-block" - href={Routes.settings_path(PlausibleWeb.Endpoint, :subscription)} - > - account settings - - and upgrade your subscription to accept the site ownership. -

    - - <.notice - :if={Map.get(@invitation, :no_plan)} - title="No subscription" - class="mt-4 shadow-xs dark:shadow-none" - > - You are unable to accept the ownership of this site because your account does not have a subscription. To become the owner of this site, you should upgrade to a suitable plan. - -
    -
    - <.button - :if={!(Map.get(@invitation, :exceeded_limits) || Map.get(@invitation, :no_plan))} - mt?={false} - class="w-full sm:w-auto sm:text-sm" - data-method="post" - data-csrf={Plug.CSRFProtection.get_csrf_token()} - data-to={"/sites/invitations/#{@invitation.invitation.invitation_id}/accept"} - data-autofocus - > - Accept and continue - - <.button_link - :if={Map.get(@invitation, :exceeded_limits) || Map.get(@invitation, :no_plan)} - mt?={false} - href={Routes.billing_path(PlausibleWeb.Endpoint, :choose_plan)} - class="w-full sm:w-auto sm:text-sm" - data-autofocus - > - Upgrade - - <.button_link - mt?={false} - class="w-full sm:w-auto sm:text-sm" - href="#" - theme="secondary" - data-method="post" - data-csrf={Plug.CSRFProtection.get_csrf_token()} - data-to={"/sites/invitations/#{@invitation.invitation.invitation_id}/reject"} - > - Reject - -
    -
    - """ - end - attr(:filter_text, :string, default: "") attr(:uri, URI, required: true) @@ -965,7 +800,7 @@ defmodule PlausibleWeb.Live.Sites do defp load_sites(%{assigns: assigns} = socket) do sites = - Sites.list_with_invitations(assigns.current_user, assigns.params, + Sites.list(assigns.current_user, assigns.params, filter_by_domain: assigns.filter_text, team: assigns.current_team ) @@ -991,71 +826,14 @@ defmodule PlausibleWeb.Live.Sites do do: load_consolidated_stats(assigns.consolidated_view), else: :loading - invitations = extract_invitations(sites.entries, assigns.current_team) - assign( socket, sites: sites, - invitations: invitations, hourly_stats: hourly_stats, consolidated_stats: consolidated_stats || Map.get(assigns, :consolidated_stats) ) end - defp extract_invitations(sites, team) do - sites - |> Enum.filter(&(&1.entry_type == "invitation")) - |> Enum.flat_map(& &1.invitations) - |> Enum.map(&check_limits(&1, team)) - end - - on_ee do - defp check_limits(%{role: :owner, site: site} = invitation, team) do - case ensure_can_take_ownership(site, team) do - :ok -> - check_features(invitation, team) - - {:error, :no_plan} -> - %{invitation: invitation, no_plan: true} - - {:error, {:over_plan_limits, limits}} -> - limits = PlausibleWeb.TextHelpers.pretty_list(limits) - %{invitation: invitation, exceeded_limits: limits} - end - end - end - - defp check_limits(invitation, _), do: %{invitation: invitation} - - defdelegate ensure_can_take_ownership(site, team), to: Teams.Invitations - - def check_features(%{role: :owner, site: site} = invitation, team) do - case check_feature_access(site, team) do - :ok -> - %{invitation: invitation} - - {:error, {:missing_features, features}} -> - feature_names = - features - |> Enum.map(& &1.display_name()) - |> PlausibleWeb.TextHelpers.pretty_list() - - %{invitation: invitation, missing_features: feature_names} - end - end - - defp check_feature_access(site, new_team) do - missing_features = - Teams.Billing.features_usage(nil, [site.id]) - |> Enum.filter(&(&1.check_availability(new_team) != :ok)) - - if missing_features == [] do - :ok - else - {:error, {:missing_features, missing_features}} - end - end - defp set_filter_text(socket, filter_text) do filter_text = String.trim(filter_text) uri = socket.assigns.uri @@ -1113,7 +891,7 @@ defmodule PlausibleWeb.Live.Sites do end 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 + # technically this is team not setup, but is also equivalent of having no sites at this moment, so CTA should not be shown no_consolidated_view(no_consolidated_view_reason: :no_sites) end diff --git a/test/plausible/sites_test.exs b/test/plausible/sites_test.exs index e9e0ba935cd9..97ca545f5721 100644 --- a/test/plausible/sites_test.exs +++ b/test/plausible/sites_test.exs @@ -269,7 +269,7 @@ defmodule Plausible.SitesTest do end end - describe "list/3 and list_with_invitations/3" do + describe "list/3" do test "returns empty when there are no sites" do user = new_user() _rogue_site = new_site() @@ -289,28 +289,11 @@ defmodule Plausible.SitesTest do total_entries: 0, total_pages: 1 } = Plausible.Teams.Sites.list(user, %{}) - - assert %{ - entries: [], - page_size: 24, - page_number: 1, - total_entries: 0, - total_pages: 1 - } = Sites.list_with_invitations(user, %{}) - - assert %{ - entries: [], - page_size: 24, - page_number: 1, - total_entries: 0, - total_pages: 1 - } = Plausible.Teams.Sites.list_with_invitations(user, %{}) end - test "lists guest sites, site invitations and transfers when no current team set" do + test "lists guest sites when no current team set (no invitations)" do user1 = new_user() user2 = new_user() - user3 = new_user() user4 = new_user() # owned site on a setup team @@ -325,32 +308,21 @@ defmodule Plausible.SitesTest do site2 = new_site(owner: user2, domain: "guest.example.com") add_guest(site2, user: user1, role: :editor) - # site invitation - site3 = new_site(owner: user3, domain: "invitation.example.com") - invite_guest(site3, user1, role: :viewer, inviter: user3) - - # transfer - site4 = new_site(domain: "transfer.example.com", owner: user3) - invite_transfer(site4, user1, inviter: user2) - # other team site access - site5 = new_site(domain: "team.example.com", owner: user4) - add_member(site5.team, user: user1, role: :editor) + site3 = new_site(domain: "team.example.com", owner: user4) + add_member(site3.team, user: user1, role: :editor) assert %{ entries: [ - %{domain: "invitation.example.com"}, - %{domain: "transfer.example.com"}, %{domain: "guest.example.com"} ] } = - Sites.list_with_invitations(user1, %{}) + Sites.list(user1, %{}) end - test "lists guest sites, site invitations, transfers and team sites when current team set but not setup" do + test "lists guest sites and team sites when current team set but not setup (no invitations)" do user1 = new_user() user2 = new_user() - user3 = new_user() user4 = new_user() # owned site on a personal team @@ -360,36 +332,25 @@ defmodule Plausible.SitesTest do site2 = new_site(owner: user2, domain: "guest.example.com") add_guest(site2, user: user1, role: :editor) - # site invitation - site3 = new_site(owner: user3, domain: "invitation.example.com") - invite_guest(site3, user1, role: :viewer, inviter: user3) - - # transfer - site4 = new_site(domain: "transfer.example.com", owner: user3) - invite_transfer(site4, user1, inviter: user2) - # other team site access - site5 = new_site(domain: "team.example.com", owner: user4) - add_member(site5.team, user: user1, role: :editor) + site3 = new_site(domain: "team.example.com", owner: user4) + add_member(site3.team, user: user1, role: :editor) # excluded new_site(owner: user1, domain: "consolidated.example.com", consolidated: true) assert %{ entries: [ - %{domain: "invitation.example.com"}, - %{domain: "transfer.example.com"}, %{domain: "guest.example.com"}, %{domain: "own.example.com"} ] } = - Sites.list_with_invitations(user1, %{}, team: site1.team) + Sites.list(user1, %{}, team: site1.team) end - test "lists team sites and transfers when current team set and setup" do + test "lists team sites when current team set and setup (no invitations)" do user1 = new_user() user2 = new_user() - user3 = new_user() user4 = new_user() # owned site on a personal team @@ -399,37 +360,29 @@ defmodule Plausible.SitesTest do site2 = new_site(owner: user2, domain: "guest.example.com") add_guest(site2, user: user1, role: :editor) - # site invitation - site3 = new_site(owner: user3, domain: "invitation.example.com") - invite_guest(site3, user1, role: :viewer, inviter: user3) - - # transfer - site4 = new_site(domain: "transfer.example.com", owner: user3) - invite_transfer(site4, user1, inviter: user2) - # other team site access - site5 = new_site(domain: "team.example.com", owner: user4) - team5 = Plausible.Teams.complete_setup(site5.team) - add_member(site5.team, user: user1, role: :admin) + site3 = new_site(domain: "team.example.com", owner: user4) + team3 = Plausible.Teams.complete_setup(site3.team) + add_member(site3.team, user: user1, role: :admin) # excluded new_site(owner: user1, domain: "consolidated.example.com", consolidated: true) assert %{ entries: [ - %{domain: "transfer.example.com"}, %{domain: "team.example.com"} ] } = - Sites.list_with_invitations(user1, %{}, team: team5) + Sites.list(user1, %{}, team: team3) end - test "shows both pending transfer and pinned site for user without team with guest membership" do + test "shows pinned site for user without team with guest membership (no invitation)" do owner = new_user() pending_owner = new_user() site = new_site(owner: owner, domain: "one.example.com") add_guest(site, user: pending_owner, role: :editor) + # transfer no longer shown invite_transfer(site, pending_owner, inviter: owner) {:ok, _} = Sites.toggle_pin(pending_owner, site) @@ -440,49 +393,42 @@ defmodule Plausible.SitesTest do assert %{ entries: [ - %{domain: "one.example.com", entry_type: "invitation"}, %{domain: "one.example.com", entry_type: "pinned_site"} ] } = - Sites.list_with_invitations(pending_owner, %{}) + Sites.list(pending_owner, %{}) end - test "shows both pending transfer and site for user without team with guest membership" do + test "shows site for user without team with guest membership (no invitation)" do owner = new_user() pending_owner = new_user() site = new_site(owner: owner, domain: "one.example.com") add_guest(site, user: pending_owner, role: :editor) - invite_transfer(site, pending_owner, inviter: owner) - assert %{ entries: [ - %{domain: "one.example.com", entry_type: "invitation"}, %{domain: "one.example.com", entry_type: "site"} ] } = - Sites.list_with_invitations(pending_owner, %{}) + Sites.list(pending_owner, %{}) end - test "shows both pending transfer and site for user with personal team with guest membership" do + test "shows site for user with personal team with guest membership (no invitation)" do owner = new_user() pending_owner = new_user() |> subscribe_to_growth_plan() pending_team = team_of(pending_owner) site = new_site(owner: owner, domain: "one.example.com") add_guest(site, user: pending_owner, role: :editor) - invite_transfer(site, pending_owner, inviter: owner) - assert %{ entries: [ - %{domain: "one.example.com", entry_type: "invitation"}, %{domain: "one.example.com", entry_type: "site"} ] } = - Sites.list_with_invitations(pending_owner, %{}, team: pending_team) + Sites.list(pending_owner, %{}, team: pending_team) end - test "shows only pending transfer for user with setup team with guest membership" do + test "does not show site for user with setup team with guest membership (no invitation)" do owner = new_user() pending_owner = new_user() |> subscribe_to_growth_plan() @@ -494,48 +440,38 @@ defmodule Plausible.SitesTest do site = new_site(owner: owner, domain: "one.example.com") add_guest(site, user: pending_owner, role: :editor) - invite_transfer(site, pending_owner, inviter: owner) - assert %{ - entries: [ - %{domain: "one.example.com", entry_type: "invitation"} - ] + entries: [] } = - Sites.list_with_invitations(pending_owner, %{}, team: pending_team) + Sites.list(pending_owner, %{}, team: pending_team) end test "does not show transfer for user with site in their personal team" do - owner = new_user() pending_owner = new_user() |> subscribe_to_growth_plan() pending_team = team_of(pending_owner) - site = new_site(owner: pending_owner, domain: "one.example.com") - - invite_transfer(site, pending_owner, inviter: owner) + _site = new_site(owner: pending_owner, domain: "one.example.com") assert %{ entries: [ %{domain: "one.example.com", entry_type: "site"} ] } = - Sites.list_with_invitations(pending_owner, %{}, team: pending_team) + Sites.list(pending_owner, %{}, team: pending_team) end test "does not show transfer for user with site in their setup team" do - owner = new_user() pending_owner = new_user() |> subscribe_to_growth_plan() pending_team = team_of(pending_owner) - site = new_site(owner: pending_owner, domain: "one.example.com") + _site = new_site(owner: pending_owner, domain: "one.example.com") pending_team = Plausible.Teams.complete_setup(pending_team) - invite_transfer(site, pending_owner, inviter: owner) - assert %{ entries: [ %{domain: "one.example.com", entry_type: "site"} ] } = - Sites.list_with_invitations(pending_owner, %{}, team: pending_team) + Sites.list(pending_owner, %{}, team: pending_team) end test "pinned site doesn't matter with membership revoked (no active invitations)" do @@ -552,22 +488,18 @@ defmodule Plausible.SitesTest do revoke_membership(site2, user1) assert %{entries: [%{domain: "one.example.com"}]} = Sites.list(user1, %{}) - assert %{entries: [%{domain: "one.example.com"}]} = Sites.list_with_invitations(user1, %{}) + assert %{entries: [%{domain: "one.example.com"}]} = Sites.list(user1, %{}) assert %{entries: [%{domain: "one.example.com"}]} = Plausible.Teams.Sites.list(user1, %{}) assert %{entries: [%{domain: "one.example.com"}]} = - Plausible.Teams.Sites.list_with_invitations(user1, %{}) + Plausible.Teams.Sites.list(user1, %{}) end - test "pinned site with active invitation" do + test "pinned site (no invitation shown)" do user1 = new_user(email: "user1@example.com") - user2 = new_user(email: "user2@example.com") site1 = new_site(domain: "one.example.com", owner: user1) - site2 = new_site(domain: "two.example.com") - - invite_guest(site2, user1, role: :editor, inviter: user2) {:ok, _} = Sites.toggle_pin(user1, site1) @@ -575,19 +507,18 @@ defmodule Plausible.SitesTest do assert %{ entries: [ - %{domain: "two.example.com", entry_type: "invitation"}, %{domain: "one.example.com", entry_type: "pinned_site"} ] } = - Sites.list_with_invitations(user1, %{}) + Sites.list(user1, %{}) assert %{entries: [%{domain: "one.example.com"}]} = Plausible.Teams.Sites.list(user1, %{}) - assert %{entries: [%{domain: "two.example.com"}, %{domain: "one.example.com"}]} = - Plausible.Teams.Sites.list_with_invitations(user1, %{}) + assert %{entries: [%{domain: "one.example.com"}]} = + Plausible.Teams.Sites.list(user1, %{}) end - test "pinned site on active invitation" do + test "pinned site after invitation revoked (no invitation shown)" do user1 = new_user(email: "user1@example.com") user2 = new_user(email: "user2@example.com") @@ -597,53 +528,36 @@ defmodule Plausible.SitesTest do {:ok, _} = Sites.toggle_pin(user1, site1) revoke_membership(site1, user1) - invite_guest(site1, user1, role: :editor, inviter: user2) - assert %{entries: []} = Sites.list(user1, %{}) - assert %{ - entries: [ - %{domain: "one.example.com", entry_type: "invitation"} - ] - } = - Sites.list_with_invitations(user1, %{}) - assert %{entries: []} = Plausible.Teams.Sites.list(user1, %{}) - assert %{entries: [%{domain: "one.example.com", entry_type: "invitation"}]} = - Plausible.Teams.Sites.list_with_invitations(user1, %{}) + assert %{entries: []} = + Plausible.Teams.Sites.list(user1, %{}) end - test "puts invitations first, pinned sites second, sites last" do + test "puts pinned sites first, sites last (no invitations)" do user1 = new_user() user2 = new_user() - user3 = new_user() site1 = new_site(owner: user1, domain: "one.example.com") site2 = new_site(owner: user2, domain: "two.example.com") - site3 = new_site(owner: user3, domain: "three.example.com") + site3 = new_site(domain: "three.example.com") site4 = new_site(domain: "four.example.com") - site5 = new_site(owner: user3, domain: "five.example.com") # excluded new_site(owner: user1, domain: "consolidated1.example.com", consolidated: true) new_site(owner: user2, domain: "consolidated2.example.com", consolidated: true) - new_site(owner: user3, domain: "consolidated3.example.com", consolidated: true) - invite_guest(site2, user1, role: :editor, inviter: user2) add_guest(site3, user: user1, role: :viewer) add_guest(site4, user: user1, role: :editor) - invite_transfer(site5, user1, inviter: user3) - {:ok, _} = Sites.toggle_pin(user1, site3) {:ok, _pin_to_ignore} = Sites.toggle_pin(user2, site2) site1_id = site1.id - site2_id = site2.id site3_id = site3.id site4_id = site4.id - site5_id = site5.id assert %{ entries: [ @@ -655,16 +569,14 @@ defmodule Plausible.SitesTest do assert %{ entries: [ - %{id: ^site5_id, entry_type: "invitation"}, - %{id: ^site2_id, entry_type: "invitation"}, %{id: ^site3_id, entry_type: "pinned_site"}, %{id: ^site4_id, entry_type: "site"}, %{id: ^site1_id, entry_type: "site"} ] - } = Sites.list_with_invitations(user1, %{}) + } = Sites.list(user1, %{}) end - test "pinned sites are ordered according to the time they were pinned at" do + test "pinned sites are ordered according to the time they were pinned at (no invitations)" do user1 = new_user() user2 = new_user() user3 = new_user() @@ -675,6 +587,7 @@ defmodule Plausible.SitesTest do site4 = new_site(domain: "four.example.com") site5 = new_site(owner: user3, domain: "five.example.com") + # invitations no longer shown invite_guest(site2, user1, role: :editor, inviter: user2) add_guest(site3, user: user1, role: :viewer) add_guest(site4, user: user1, role: :editor) @@ -684,10 +597,8 @@ defmodule Plausible.SitesTest do {:ok, _} = Sites.toggle_pin(user1, site3) site1_id = site1.id - site2_id = site2.id site3_id = site3.id site4_id = site4.id - site5_id = site5.id Sites.set_option(user1, site1, :pinned_at, ~N[2023-10-22 12:00:00]) {:ok, _} = Sites.toggle_pin(user1, site3) @@ -702,32 +613,21 @@ defmodule Plausible.SitesTest do assert %{ entries: [ - %{id: ^site5_id, entry_type: "invitation"}, - %{id: ^site2_id, entry_type: "invitation"}, %{id: ^site3_id, entry_type: "pinned_site"}, %{id: ^site1_id, entry_type: "pinned_site"}, %{id: ^site4_id, entry_type: "site"} ] - } = Sites.list_with_invitations(user1, %{}) + } = Sites.list(user1, %{}) end - test "filters by domain" do + test "filters by domain (no invitations)" do user1 = new_user() - user2 = new_user() - user3 = new_user() site1 = new_site(owner: user1, domain: "first.example.com") - site2 = new_site(owner: user2, domain: "first-transfer.example.com") - site3 = new_site(owner: user3, domain: "first-invitation.example.com") - _site4 = new_site(owner: user1, domain: "another.example.com") + _site2 = new_site(owner: user1, domain: "another.example.com") new_site(owner: user1, domain: "consolidated.example.com", consolidated: true) - invite_guest(site3, user1, role: :viewer, inviter: user3) - invite_transfer(site2, user1, inviter: user2) - site1_id = site1.id - site2_id = site2.id - site3_id = site3.id assert %{ entries: [ @@ -737,25 +637,17 @@ defmodule Plausible.SitesTest do assert %{ entries: [ - %{id: ^site3_id}, - %{id: ^site2_id}, %{id: ^site1_id} ] - } = Sites.list_with_invitations(user1, %{}, filter_by_domain: "first") + } = Sites.list(user1, %{}, filter_by_domain: "first") end - test "scopes by team when provided" do + test "scopes by team when provided (no invitations)" do user1 = new_user() - user2 = new_user() - user3 = new_user() site1 = new_site(owner: user1, domain: "first.example.com") - site2 = new_site(owner: user2, domain: "first-transfer.example.com") - site3 = new_site(owner: user3, domain: "first-invitation.example.com") site4 = new_site(domain: "zzzsitefromanotherteam.com") - invite_guest(site3, user1, role: :viewer, inviter: user3) - invite_transfer(site2, user1, inviter: user2) team4 = Plausible.Teams.complete_setup(site4.team) add_member(team4, user: user1, role: :admin) @@ -767,11 +659,9 @@ defmodule Plausible.SitesTest do assert_matches %{ entries: [ - %{id: ^site3.id}, - %{id: ^site2.id}, %{id: ^site1.id} ] - } = Sites.list_with_invitations(user1, %{}) + } = Sites.list(user1, %{}) assert_matches %{ entries: [ @@ -781,36 +671,26 @@ defmodule Plausible.SitesTest do assert_matches %{ entries: [ - %{id: ^site2.id}, %{id: ^site4.id} ] - } = Sites.list_with_invitations(user1, %{}, team: team4) + } = Sites.list(user1, %{}, team: team4) end - test "handles pagination correctly" do + test "handles pagination correctly (no invitations)" do user1 = new_user() - user2 = new_user() - user3 = new_user() site1 = new_site(owner: user1, domain: "one.example.com") - site2 = new_site(owner: user2, domain: "two.example.com") site3 = new_site(domain: "three.example.com") site4 = new_site(domain: "four.example.com") - site5 = new_site(owner: user3, domain: "five.example.com") - invite_guest(site2, user1, role: :editor, inviter: user2) add_guest(site3, user: user1, role: :viewer) add_guest(site4, user: user1, role: :editor) - invite_transfer(site5, user1, inviter: user3) - {:ok, _} = Sites.toggle_pin(user1, site3) site1_id = site1.id - site2_id = site2.id site3_id = site3.id site4_id = site4.id - site5_id = site5.id assert %{ entries: [%{id: ^site3_id}, %{id: ^site4_id}], @@ -835,32 +715,6 @@ defmodule Plausible.SitesTest do total_entries: 3, total_pages: 1 } = Sites.list(user1, %{"page_size" => 3}) - - # list_with_invitations - # - assert %{ - entries: [%{id: ^site5_id}, %{id: ^site2_id}], - page_number: 1, - page_size: 2, - total_entries: 5, - total_pages: 3 - } = Sites.list_with_invitations(user1, %{"page_size" => 2}) - - assert %{ - entries: [%{id: ^site3_id}, %{id: ^site4_id}], - page_number: 2, - page_size: 2, - total_entries: 5, - total_pages: 3 - } = Sites.list_with_invitations(user1, %{"page_size" => 2, "page" => 2}) - - assert %{ - entries: [%{id: ^site1_id}], - page_number: 3, - page_size: 2, - total_entries: 5, - total_pages: 3 - } = Sites.list_with_invitations(user1, %{"page_size" => 2, "page" => 3}) end end diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index 2be757b4c93b..31cabe186f91 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -146,25 +146,6 @@ defmodule PlausibleWeb.SiteControllerTest do assert site_card =~ site.domain end - test "shows invitations for user by email address", %{conn: conn, user: user} do - inviter = new_user() - site = new_site(owner: inviter) - invite_guest(site, user, inviter: inviter, role: :editor) - conn = get(conn, "/sites") - - assert html_response(conn, 200) =~ site.domain - end - - test "invitations are case insensitive", %{conn: conn, user: user} do - inviter = new_user() - site = new_site(owner: inviter) - invite_guest(site, String.upcase(user.email), inviter: inviter, role: :editor) - - conn = get(conn, "/sites") - - assert html_response(conn, 200) =~ site.domain - end - test "paginates sites", %{conn: initial_conn, user: user} do for i <- 1..25 do new_site( @@ -234,21 +215,21 @@ defmodule PlausibleWeb.SiteControllerTest do end test "filters by domain", %{conn: conn, user: user} do - _site1 = new_site(domain: "first.example.com", owner: user) - _site2 = new_site(domain: "second.example.com", owner: user) + _site1 = new_site(domain: "alpha.example.com", owner: user) + _site2 = new_site(domain: "beta.example.com", owner: user) _rogue_site = new_site() inviter = new_user() - new_site(owner: inviter, domain: "first-another.example.com") - |> invite_guest(user, inviter: inviter, role: :viewer) + site3 = new_site(owner: inviter, domain: "alpha-another.example.com") + add_guest(site3, user: user, role: :viewer) - conn = get(conn, "/sites", filter_text: "first") + conn = get(conn, "/sites", filter_text: "alpha") resp = html_response(conn, 200) - assert resp =~ "first.example.com" - assert resp =~ "first-another.example.com" - refute resp =~ "second.example.com" + assert resp =~ "alpha.example.com" + assert resp =~ "alpha-another.example.com" + refute resp =~ "beta.example.com" end test "does not show empty state when filter returns empty but there are sites", %{ diff --git a/test/plausible_web/live/sites_test.exs b/test/plausible_web/live/sites_test.exs index e6d83d21550b..96deed3efe98 100644 --- a/test/plausible_web/live/sites_test.exs +++ b/test/plausible_web/live/sites_test.exs @@ -51,63 +51,6 @@ defmodule PlausibleWeb.Live.SitesTest do assert element_exists?(html, ~s|a[data-test-id="team-settings-link"]|) end - test "renders team invitations", %{user: user, conn: conn} do - owner1 = new_user(name: "G.I. Joe") - new_site(owner: owner1) - team1 = team_of(owner1) - - owner2 = new_user(name: "G.I. Jane") - new_site(owner: owner2) - team2 = team_of(owner2) - - invitation1 = invite_member(team1, user, inviter: owner1, role: :viewer) - invitation2 = invite_member(team2, user, inviter: owner2, role: :editor) - - {:ok, _lv, html} = live(conn, "/sites") - - assert text_of_element(html, "#invitation-#{invitation1.invitation_id}") =~ - "G.I. Joe has invited you to join the \"My personal sites\" as viewer member." - - assert text_of_element(html, "#invitation-#{invitation2.invitation_id}") =~ - "G.I. Jane has invited you to join the \"My personal sites\" as editor member." - - assert element_exists?( - html, - ~s|#invitation-#{invitation1.invitation_id} a[href="#{Routes.invitation_path(PlausibleWeb.Endpoint, :accept_invitation, invitation1.invitation_id)}"]| - ) - - assert element_exists?( - html, - ~s|#invitation-#{invitation1.invitation_id} a[href="#{Routes.invitation_path(PlausibleWeb.Endpoint, :reject_invitation, invitation1.invitation_id)}"]| - ) - - assert element_exists?( - html, - ~s|#invitation-#{invitation2.invitation_id} a[href="#{Routes.invitation_path(PlausibleWeb.Endpoint, :accept_invitation, invitation2.invitation_id)}"]| - ) - - assert element_exists?( - html, - ~s|#invitation-#{invitation2.invitation_id} a[href="#{Routes.invitation_path(PlausibleWeb.Endpoint, :reject_invitation, invitation2.invitation_id)}"]| - ) - end - - @tag :ee_only - test "renders ownership transfer invitation for a case with no plan", %{ - conn: conn, - user: user - } do - inviter = new_user() - site = new_site(owner: inviter) - - transfer = invite_transfer(site, user, inviter: inviter) - - {:ok, _lv, html} = live(conn, "/sites") - - assert text_of_element(html, "#invitation-modal-#{transfer.transfer_id}") =~ - "You are unable to accept the ownership of this site because your account does not have a subscription" - end - @tag :ee_only test "renders upgrade nag when current team has a site and trial expired", %{ conn: conn, @@ -124,27 +67,6 @@ defmodule PlausibleWeb.Live.SitesTest do assert html =~ "Payment required" end - @tag :ee_only - test "renders upgrade nag when there's a pending transfer", %{ - conn: conn, - user: user - } do - {:ok, personal_team} = Plausible.Teams.get_or_create(user) - - another_user = new_user() - site = new_site(owner: another_user) - - personal_team - |> Ecto.Changeset.change(trial_expiry_date: Date.add(Date.utc_today(), -1)) - |> Repo.update!() - - invite_transfer(site, user, inviter: another_user) - - {:ok, _lv, html} = live(conn, "/sites") - - assert html =~ "Payment required" - end - @tag :ee_only test "does not render upgrade nag when there's no current team", %{conn: conn, user: user} do team = new_site().team |> Plausible.Teams.complete_setup() @@ -156,20 +78,7 @@ defmodule PlausibleWeb.Live.SitesTest do end @tag :ee_only - test "does not render upgrade nag when current team has no sites and user has no pending transfers", - %{conn: conn, user: user} do - {:ok, _personal_team} = Plausible.Teams.get_or_create(user) - - team = new_site().team |> Plausible.Teams.complete_setup() - add_member(team, user: user, role: :owner) - - {:ok, _lv, html} = live(conn, "/sites") - - refute html =~ "Payment required" - end - - @tag :ee_only - test "does not render upgrade nag if current team does not have any sites yet and user has no pending transfers", + test "does not render upgrade nag if current team does not have any sites yet", %{conn: conn, user: user} do {:ok, personal_team} = Plausible.Teams.get_or_create(user) @@ -185,44 +94,6 @@ defmodule PlausibleWeb.Live.SitesTest do refute html =~ "Payment required" end - @tag :ee_only - test "renders ownership transfer invitation for a case with exceeded limits", %{ - conn: conn, - user: user - } do - inviter = new_user() - site = new_site(owner: inviter) - - transfer = invite_transfer(site, user, inviter: inviter) - - # fill site quota - subscribe_to_growth_plan(user) - for _ <- 1..10, do: new_site(owner: user) - - {:ok, _lv, html} = live(conn, "/sites") - - assert text_of_element(html, "#invitation-modal-#{transfer.transfer_id}") =~ - "Owning this site would exceed your site limit" - end - - @tag :ee_only - test "renders ownership transfer invitation for a case with missing features", %{ - conn: conn, - user: user - } do - inviter = new_user() - site = new_site(owner: inviter, allowed_event_props: ["dummy"]) - - transfer = invite_transfer(site, user, inviter: inviter) - - subscribe_to_growth_plan(user) - - {:ok, _lv, html} = live(conn, "/sites") - - assert text_of_element(html, "#invitation-modal-#{transfer.transfer_id}") =~ - "The site uses Custom Properties, which your current subscription does not support" - end - test "renders 24h visitors correctly", %{conn: conn, user: user} do site = new_site(owner: user)