diff --git a/assets/js/dashboard.tsx b/assets/js/dashboard.tsx index 8232cb50ae..dce77243d5 100644 --- a/assets/js/dashboard.tsx +++ b/assets/js/dashboard.tsx @@ -64,12 +64,21 @@ if (container && container.dataset) { ? { loggedIn: true, id: parseInt(container.dataset.currentUserId!, 10), - role: container.dataset.currentUserRole as Role + role: container.dataset.currentUserRole as Role, + team: { + identifier: container.dataset.teamIdentifier ?? null, + hasConsolidatedView: + container.dataset.teamHasConsolidatedView === 'true' + } } : { loggedIn: false, id: null, - role: container.dataset.currentUserRole as Role + role: container.dataset.currentUserRole as Role, + team: { + identifier: null, + hasConsolidatedView: false + } } } > diff --git a/assets/js/dashboard/components/filter-operator-selector.js b/assets/js/dashboard/components/filter-operator-selector.js index 1e0a29610d..980c323242 100644 --- a/assets/js/dashboard/components/filter-operator-selector.js +++ b/assets/js/dashboard/components/filter-operator-selector.js @@ -76,9 +76,7 @@ export default function FilterOperatorSelector(props) { 'w-full text-left ', popover.items.classNames.navigationLink, popover.items.classNames.selectedOption, - popover.items.classNames.hoverLink, - popover.items.classNames.roundedStart, - popover.items.classNames.roundedEnd + popover.items.classNames.hoverLink )} > {FILTER_OPERATIONS_DISPLAY_NAMES[operation]} diff --git a/assets/js/dashboard/components/popover.tsx b/assets/js/dashboard/components/popover.tsx index d457348047..56662badb5 100644 --- a/assets/js/dashboard/components/popover.tsx +++ b/assets/js/dashboard/components/popover.tsx @@ -26,7 +26,7 @@ const transition = { const panel = { classNames: { roundedSheet: - 'focus:outline-hidden rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black/5 font-medium text-gray-800 dark:text-gray-200' + 'flex flex-col gap-0.5 p-1 focus:outline-hidden rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black/5 font-medium text-gray-800 dark:text-gray-200' } } @@ -48,10 +48,16 @@ const items = { classNames: { navigationLink: classNames( 'flex items-center justify-between', - 'px-4 py-2 text-sm leading-tight', + 'px-4 py-2.5 text-sm leading-tight rounded-md', 'cursor-pointer' ), - selectedOption: classNames('data-[selected=true]:font-bold'), + selectedOption: classNames( + 'data-[selected=true]:bg-gray-100', + 'data-[selected=true]:dark:bg-gray-700', + 'data-[selected=true]:text-gray-900', + 'data-[selected=true]:dark:text-gray-100', + 'data-[selected=true]:font-semibold' + ), hoverLink: classNames( 'hover:bg-gray-100', 'hover:text-gray-900', @@ -62,10 +68,7 @@ const items = { 'focus-within:text-gray-900', 'dark:focus-within:bg-gray-700', 'dark:focus-within:text-gray-100' - ), - roundedStart: 'first-of-type:rounded-t-md', - roundedEnd: 'last-of-type:rounded-b-md', - groupRoundedEnd: 'group-last-of-type:rounded-b-md' + ) } } diff --git a/assets/js/dashboard/components/tabs.tsx b/assets/js/dashboard/components/tabs.tsx index 081f66ef7e..241159fd78 100644 --- a/assets/js/dashboard/components/tabs.tsx +++ b/assets/js/dashboard/components/tabs.tsx @@ -145,12 +145,7 @@ const Items = ({ 'w-full text-left', popover.items.classNames.navigationLink, popover.items.classNames.selectedOption, - popover.items.classNames.hoverLink, - { - [popover.items.classNames.roundedStart]: !searchable || !showSearch - // when the menu is not searchable, the first item needs rounded top - }, - popover.items.classNames.roundedEnd + popover.items.classNames.hoverLink ) return ( diff --git a/assets/js/dashboard/filtering/segments.test.ts b/assets/js/dashboard/filtering/segments.test.ts index 6f5100f3c1..a52eac22fd 100644 --- a/assets/js/dashboard/filtering/segments.test.ts +++ b/assets/js/dashboard/filtering/segments.test.ts @@ -90,7 +90,12 @@ describe(`${isListableSegment.name}`, () => { const site: Pick = { siteSegmentsAvailable: true } - const user: UserContextValue = { loggedIn: true, id: 1, role: Role.editor } + const user: UserContextValue = { + loggedIn: true, + id: 1, + role: Role.editor, + team: { identifier: null, hasConsolidatedView: false } + } it('should return true for site segment when siteSegmentsAvailable is true', () => { const segment = { id: 1, type: SegmentType.site, owner_id: 1 } @@ -103,7 +108,12 @@ describe(`${isListableSegment.name}`, () => { isListableSegment({ segment, site, - user: { loggedIn: false, role: Role.public, id: null } + user: { + loggedIn: false, + role: Role.public, + id: null, + team: { identifier: null, hasConsolidatedView: false } + } }) ).toBe(false) }) @@ -175,7 +185,12 @@ describe(`${resolveFilters.name}`, () => { describe(`${canSeeSegmentDetails.name}`, () => { it('should return true if the user is logged in and not a public role', () => { - const user: UserContextValue = { loggedIn: true, role: Role.admin, id: 1 } + const user: UserContextValue = { + loggedIn: true, + role: Role.admin, + id: 1, + team: { identifier: null, hasConsolidatedView: false } + } expect(canSeeSegmentDetails({ user })).toBe(true) }) @@ -183,13 +198,19 @@ describe(`${canSeeSegmentDetails.name}`, () => { const user: UserContextValue = { loggedIn: false, role: Role.editor, - id: null + id: null, + team: { identifier: null, hasConsolidatedView: false } } expect(canSeeSegmentDetails({ user })).toBe(false) }) it('should return false if the user has a public role', () => { - const user: UserContextValue = { loggedIn: true, role: Role.public, id: 1 } + const user: UserContextValue = { + loggedIn: true, + role: Role.public, + id: 1, + team: { identifier: null, hasConsolidatedView: false } + } expect(canSeeSegmentDetails({ user })).toBe(false) }) }) diff --git a/assets/js/dashboard/keybinding.tsx b/assets/js/dashboard/keybinding.tsx index eea3626ee4..f738c8395d 100644 --- a/assets/js/dashboard/keybinding.tsx +++ b/assets/js/dashboard/keybinding.tsx @@ -145,7 +145,7 @@ export function KeybindHint({ return ( diff --git a/assets/js/dashboard/nav-menu/filters-bar.tsx b/assets/js/dashboard/nav-menu/filters-bar.tsx index c34e7aeafd..d91a57e831 100644 --- a/assets/js/dashboard/nav-menu/filters-bar.tsx +++ b/assets/js/dashboard/nav-menu/filters-bar.tsx @@ -309,10 +309,6 @@ const SeeMoreMenu = ({ popover.items.classNames.navigationLink, popover.items.classNames.selectedOption, popover.items.classNames.hoverLink, - { - [popover.items.classNames.roundedStart]: !showMoreFilters // rounded start is needed when there's no filters panel above - }, - popover.items.classNames.roundedEnd, 'whitespace-nowrap' ) diff --git a/assets/js/dashboard/nav-menu/nav-menu-components.tsx b/assets/js/dashboard/nav-menu/nav-menu-components.tsx index 3f3b19bac7..95952c223e 100644 --- a/assets/js/dashboard/nav-menu/nav-menu-components.tsx +++ b/assets/js/dashboard/nav-menu/nav-menu-components.tsx @@ -1,5 +1,5 @@ import React from 'react' export const MenuSeparator = () => ( -
+
) diff --git a/assets/js/dashboard/nav-menu/query-periods/shared-menu-items.tsx b/assets/js/dashboard/nav-menu/query-periods/shared-menu-items.tsx index f2ab80a4d0..aacf0d5c57 100644 --- a/assets/js/dashboard/nav-menu/query-periods/shared-menu-items.tsx +++ b/assets/js/dashboard/nav-menu/query-periods/shared-menu-items.tsx @@ -7,9 +7,7 @@ import { Popover, Transition } from '@headlessui/react' export const linkClassName = classNames( popover.items.classNames.navigationLink, popover.items.classNames.selectedOption, - popover.items.classNames.hoverLink, - popover.items.classNames.roundedStart, - popover.items.classNames.roundedEnd + popover.items.classNames.hoverLink ) export const datemenuButtonClassName = classNames( diff --git a/assets/js/dashboard/nav-menu/segments/searchable-segments-section.tsx b/assets/js/dashboard/nav-menu/segments/searchable-segments-section.tsx index 02a3aee997..ce5aea8b2b 100644 --- a/assets/js/dashboard/nav-menu/segments/searchable-segments-section.tsx +++ b/assets/js/dashboard/nav-menu/segments/searchable-segments-section.tsx @@ -25,8 +25,7 @@ import { useSearchableItems } from '../../hooks/use-searchable-items' const linkClassName = classNames( popover.items.classNames.navigationLink, popover.items.classNames.selectedOption, - popover.items.classNames.hoverLink, - popover.items.classNames.groupRoundedEnd + popover.items.classNames.hoverLink ) const INITIAL_SEGMENTS_SHOWN = 5 diff --git a/assets/js/dashboard/nav-menu/segments/segment-menu.tsx b/assets/js/dashboard/nav-menu/segments/segment-menu.tsx index b84cc5356d..60368fc435 100644 --- a/assets/js/dashboard/nav-menu/segments/segment-menu.tsx +++ b/assets/js/dashboard/nav-menu/segments/segment-menu.tsx @@ -20,9 +20,7 @@ import { DashboardQuery } from '../../query' const linkClassName = classNames( popover.items.classNames.navigationLink, popover.items.classNames.selectedOption, - popover.items.classNames.hoverLink, - popover.items.classNames.roundedStart, - popover.items.classNames.roundedEnd + popover.items.classNames.hoverLink ) const buttonClassName = classNames( 'text-white font-medium bg-indigo-600 hover:bg-indigo-700' diff --git a/assets/js/dashboard/nav-menu/top-bar.test.tsx b/assets/js/dashboard/nav-menu/top-bar.test.tsx index b091c6f535..7756864e2b 100644 --- a/assets/js/dashboard/nav-menu/top-bar.test.tsx +++ b/assets/js/dashboard/nav-menu/top-bar.test.tsx @@ -60,12 +60,12 @@ test('user can open and close site switcher', async () => { .map((el) => ({ text: el.textContent, href: el.getAttribute('href') })) ).toEqual( [ + { text: ['Back to sites'], href: '/sites' }, { text: ['Site settings'], href: `/${domain}/settings/general` }, { text: ['dummy.site', '1'], href: '#' }, { text: ['example.com', '2'], href: `/example.com` }, { text: ['blog.example.com', '3'], href: `/blog.example.com` }, - { text: ['aççented.ca', '4'], href: `/a%C3%A7%C3%A7ented.ca` }, - { text: ['View all'], href: '/sites' } + { text: ['aççented.ca', '4'], href: `/a%C3%A7%C3%A7ented.ca` } ].map((l) => ({ ...l, text: l.text.join('') })) ) diff --git a/assets/js/dashboard/segments/segment-modals.test.tsx b/assets/js/dashboard/segments/segment-modals.test.tsx index 3adc096d31..e9cbca078b 100644 --- a/assets/js/dashboard/segments/segment-modals.test.tsx +++ b/assets/js/dashboard/segments/segment-modals.test.tsx @@ -50,7 +50,12 @@ describe('Segment details modal - errors', () => { case: 'segment is not in list', segments: [anyPersonalSegment, anySiteSegment], segmentId: 202020, - user: { loggedIn: true, id: 1, role: Role.owner }, + user: { + loggedIn: true, + id: 1, + role: Role.owner, + team: { identifier: null, hasConsolidatedView: false } + }, message: `Segment not found with with ID "202020"`, siteOptions: { siteSegmentsAvailable: true } }, @@ -58,7 +63,12 @@ describe('Segment details modal - errors', () => { case: 'site segment is in list but not listable because site segments are not available', segments: [anyPersonalSegment, anySiteSegment], segmentId: anySiteSegment.id, - user: { loggedIn: true, id: 1, role: Role.owner }, + user: { + loggedIn: true, + id: 1, + role: Role.owner, + team: { identifier: null, hasConsolidatedView: false } + }, message: `Segment not found with with ID "${anySiteSegment.id}"`, siteOptions: { siteSegmentsAvailable: false } }, @@ -66,7 +76,12 @@ describe('Segment details modal - errors', () => { case: 'personal segment is in list but not listable because it is a public dashboard', segments: [{ ...anyPersonalSegment, owner_id: null, owner_name: null }], segmentId: anyPersonalSegment.id, - user: { loggedIn: false, id: null, role: Role.public }, + user: { + loggedIn: false, + id: null, + role: Role.public, + team: { identifier: null, hasConsolidatedView: false } + }, message: `Segment not found with with ID "${anyPersonalSegment.id}"`, siteOptions: { siteSegmentsAvailable: true } }, @@ -74,7 +89,12 @@ describe('Segment details modal - errors', () => { case: 'segment is in list and listable, but detailed view is not available because user is not logged in', segments: [{ ...anySiteSegment, owner_id: null, owner_name: null }], segmentId: anySiteSegment.id, - user: { loggedIn: false, id: null, role: Role.public }, + user: { + loggedIn: false, + id: null, + role: Role.public, + team: { identifier: null, hasConsolidatedView: false } + }, message: 'Not enough permissions to see segment details', siteOptions: { siteSegmentsAvailable: true } } @@ -118,7 +138,12 @@ describe('Segment details modal - other cases', () => { render(, { wrapper: (props) => ( { realtime: ['minute'], year: ['day', 'week', 'month'] }, - shared: false + shared: false, + isConsolidatedView: false } it('parses from dom string map correctly', () => { diff --git a/assets/js/dashboard/site-context.tsx b/assets/js/dashboard/site-context.tsx index 90a13f94a6..340c703f8f 100644 --- a/assets/js/dashboard/site-context.tsx +++ b/assets/js/dashboard/site-context.tsx @@ -21,7 +21,8 @@ export function parseSiteFromDataset(dataset: DOMStringMap): PlausibleSite { isDbip: dataset.isDbip === 'true', flags: JSON.parse(dataset.flags!), validIntervalsByPeriod: JSON.parse(dataset.validIntervalsByPeriod!), - shared: !!dataset.sharedLinkAuth + shared: !!dataset.sharedLinkAuth, + isConsolidatedView: dataset.isConsolidatedView === 'true' } } @@ -51,7 +52,8 @@ const siteContextDefaultValue = { isDbip: false, flags: {} as FeatureFlags, validIntervalsByPeriod: {} as Record>, - shared: false + shared: false, + isConsolidatedView: false } export type PlausibleSite = typeof siteContextDefaultValue diff --git a/assets/js/dashboard/site-switcher.tsx b/assets/js/dashboard/site-switcher.tsx index d42a475610..53bfcd3832 100644 --- a/assets/js/dashboard/site-switcher.tsx +++ b/assets/js/dashboard/site-switcher.tsx @@ -4,7 +4,7 @@ import React, { useRef } from 'react' import { Popover, Transition } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/20/solid' -import { Cog8ToothIcon } from '@heroicons/react/24/outline' +import { Cog8ToothIcon, ArrowLeftIcon } from '@heroicons/react/24/outline' import classNames from 'classnames' import { isModifierPressed, isTyping, Keybind, KeybindHint } from './keybinding' import { popover, BlurMenuButtonOnEscape } from './components/popover' @@ -39,12 +39,44 @@ const Favicon = ({ /> ) +const GlobeIcon = ({ className }: { className?: string }) => ( + + + + +) + const menuItemClassName = classNames( popover.items.classNames.navigationLink, popover.items.classNames.selectedOption, - popover.items.classNames.hoverLink, - popover.items.classNames.roundedStart, - popover.items.classNames.roundedEnd + popover.items.classNames.hoverLink +) + +const buttonLinkClassName = classNames( + 'flex-1 flex items-center justify-center', + 'my-1 mx-1', + 'border border-gray-300 dark:border-gray-700', + 'px-3 py-2 text-sm font-medium rounded-md', + 'bg-white text-gray-700 dark:text-gray-300 dark:bg-gray-700', + 'transition-all duration-200', + 'hover:text-gray-900 hover:border-gray-400/70 dark:hover:bg-gray-600 dark:hover:border-gray-600 dark:hover:text-white' ) const getSwitchToSiteURL = ( @@ -110,6 +142,30 @@ export const SiteSwitcher = () => { /> ))} + {!!dashboardRouteMatch && + !modal && + user.team?.hasConsolidatedView && + user.team.identifier && ( + { + const url = getSwitchToSiteURL(currentSite, { + domain: user.team.identifier! + }) + if (!url) { + closePopover() + } else { + closePopover() + window.location.assign(url) + } + }} + shouldIgnoreWhen={[isModifierPressed, isTyping]} + targetRef="document" + /> + )} + { )} title={currentSite.domain} > - + {currentSite.isConsolidatedView ? ( + + ) : ( + + )} @@ -140,18 +202,24 @@ export const SiteSwitcher = () => { data-testid="sitemenu" className={classNames(popover.panel.classNames.roundedSheet)} > - {canSeeSiteSettings && ( - <> +
+ {canSeeViewAllSites && ( + + + Back to sites + + )} + {canSeeSiteSettings && ( - - Site settings + + Site settings - - - )} + )} +
+ {(canSeeSiteSettings || canSeeViewAllSites) && } {sitesQuery.isLoading && (
@@ -167,6 +235,22 @@ export const SiteSwitcher = () => { />
)} + {user.team.hasConsolidatedView && user.team.identifier && ( + closePopover()} + > + + All sites + 0 + + )} {!!sitesInDropdown && sitesInDropdown.map(({ domain }, index) => ( { )} ))} - {canSeeViewAllSites && ( - <> - - - View all - - - )} diff --git a/assets/js/dashboard/stats/graph/interval-picker.tsx b/assets/js/dashboard/stats/graph/interval-picker.tsx index 2761eee356..afa3f1cf91 100644 --- a/assets/js/dashboard/stats/graph/interval-picker.tsx +++ b/assets/js/dashboard/stats/graph/interval-picker.tsx @@ -172,8 +172,6 @@ export function IntervalPicker({ popover.items.classNames.navigationLink, popover.items.classNames.selectedOption, popover.items.classNames.hoverLink, - popover.items.classNames.roundedStart, - popover.items.classNames.roundedEnd, 'w-full text-left' )} > diff --git a/assets/js/dashboard/user-context.tsx b/assets/js/dashboard/user-context.tsx index 8549748224..e178136175 100644 --- a/assets/js/dashboard/user-context.tsx +++ b/assets/js/dashboard/user-context.tsx @@ -12,10 +12,24 @@ export enum Role { const userContextDefaultValue = { loggedIn: false, id: null, - role: Role.public + role: Role.public, + team: { + identifier: null, + hasConsolidatedView: false + } } as - | { loggedIn: false; id: null; role: Role } - | { loggedIn: true; id: number; role: Role } + | { + loggedIn: false + id: null + role: Role + team: { identifier: null; hasConsolidatedView: false } + } + | { + loggedIn: true + id: number + role: Role + team: { identifier: string | null; hasConsolidatedView: boolean } + } export type UserContextValue = typeof userContextDefaultValue diff --git a/assets/test-utils/app-context-providers.tsx b/assets/test-utils/app-context-providers.tsx index 9c11a5579c..17f4bf4b3b 100644 --- a/assets/test-utils/app-context-providers.tsx +++ b/assets/test-utils/app-context-providers.tsx @@ -42,7 +42,8 @@ export const DEFAULT_SITE: PlausibleSite = { isDbip: false, flags: {}, validIntervalsByPeriod: {}, - shared: false + shared: false, + isConsolidatedView: false } export const TestContextProviders = ({ @@ -68,7 +69,14 @@ export const TestContextProviders = ({ // not interactive component, default value is suitable + false + end + end + @spec reset_if_enabled(Team.t()) :: :ok def reset_if_enabled(%Team{} = team) do case get(team) do diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index 6dfc6d86d8..f7abca3328 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -61,8 +61,15 @@ defmodule PlausibleWeb.StatsController do demo = site.domain == "plausible.io" dogfood_page_path = if demo, do: "/#{site.domain}", else: "/:dashboard" + consolidated_view? = Plausible.Sites.consolidated?(site) + + team_has_consolidated_view? = + on_ee(do: Plausible.ConsolidatedView.ok_to_display?(site.team, current_user), else: false) + + team_identifier = site.team.identifier + skip_to_dashboard? = - conn.params["skip_to_dashboard"] == "true" or Plausible.Sites.consolidated?(site) + conn.params["skip_to_dashboard"] == "true" or consolidated_view? {:ok, segments} = Plausible.Segments.get_all_for_site(site, site_role) @@ -87,7 +94,10 @@ defmodule PlausibleWeb.StatsController do is_dbip: is_dbip(), segments: segments, load_dashboard_js: true, - hide_footer?: if(ce?() || demo, do: false, else: site_role != :public) + hide_footer?: if(ce?() || demo, do: false, else: site_role != :public), + consolidated_view?: consolidated_view?, + team_has_consolidated_view?: team_has_consolidated_view?, + team_identifier: team_identifier ) !stats_start_date && can_see_stats? -> @@ -392,6 +402,16 @@ 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 + ) + + team_identifier = shared_link.site.team.identifier + conn |> put_resp_header("x-robots-tag", "noindex, nofollow") |> delete_resp_header("x-frame-options") @@ -414,7 +434,10 @@ defmodule PlausibleWeb.StatsController do is_dbip: is_dbip(), segments: segments, load_dashboard_js: true, - hide_footer?: if(ce?(), do: embedded?, else: embedded? || site_role != :public) + hide_footer?: if(ce?(), do: embedded?, else: embedded? || site_role != :public), + consolidated_view?: consolidated_view?, + team_has_consolidated_view?: team_has_consolidated_view?, + team_identifier: team_identifier ) end end diff --git a/lib/plausible_web/live/sites.ex b/lib/plausible_web/live/sites.ex index fcaec47546..571fd01af2 100644 --- a/lib/plausible_web/live/sites.ex +++ b/lib/plausible_web/live/sites.ex @@ -115,7 +115,7 @@ defmodule PlausibleWeb.Live.Sites do
    <.consolidated_view_card - :if={@consolidated_view && Plausible.Auth.is_super_admin?(@current_user)} + :if={@consolidated_view && consolidated_view_ok_to_display?(@current_team, @current_user)} can_manage_consolidated_view?={@can_manage_consolidated_view?} consolidated_view={@consolidated_view} consolidated_stats={@consolidated_stats} @@ -272,7 +272,7 @@ defmodule PlausibleWeb.Live.Sites do <.consolidated_view_stat value={@consolidated_stats.views_per_visit} label="Views per visit" - change={1} + change={@consolidated_stats.views_per_visit_change} />
@@ -951,6 +951,10 @@ defmodule PlausibleWeb.Live.Sites do on_ee do alias Plausible.ConsolidatedView + defp consolidated_view_ok_to_display?(team, user) do + ConsolidatedView.ok_to_display?(team, user) + end + defp init_consolidated_view_assigns(_user, nil), do: @no_consolidated_view defp init_consolidated_view_assigns(user, team) do @@ -975,6 +979,7 @@ 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 load_consolidated_stats(_consolidated_view), do: nil end diff --git a/lib/plausible_web/templates/stats/stats.html.heex b/lib/plausible_web/templates/stats/stats.html.heex index 54ef5e5467..14c034d907 100644 --- a/lib/plausible_web/templates/stats/stats.html.heex +++ b/lib/plausible_web/templates/stats/stats.html.heex @@ -49,6 +49,9 @@ data-valid-intervals-by-period={ 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-team-identifier={@team_identifier} >
diff --git a/test/plausible/stats/consolidated_view_sync_test.exs b/test/plausible/stats/consolidated_view_sync_test.exs new file mode 100644 index 0000000000..5a50dfd988 --- /dev/null +++ b/test/plausible/stats/consolidated_view_sync_test.exs @@ -0,0 +1,55 @@ +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_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index 9dc543efe4..a2e6783aeb 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -29,6 +29,9 @@ defmodule PlausibleWeb.StatsControllerTest do assert text_of_attr(resp, @react_container, "data-current-user-role") == "public" 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-team-identifier") == site.team.identifier assert "noindex, nofollow" == resp