Update site switcher UI to accommodate for consolidated view (#5838)

* Update site switcher UI to accommodate for consolidated view

* Implement logic to display consolidated view in site picker

* Fix "All sites" selected state in site switcher

* Fixup tests

* Include consolidated view assigns in shared links

* Format

* Extract `ConsolidatedView.ok_to_display?/2`

* Format

* I'll pretend no one saw this

* Skip unnecessary `on_ee`

* oops

---------

Co-authored-by: Adam Rutkowski <hq@mtod.org>
This commit is contained in:
Sanne de Vries 2025-10-29 05:58:45 -05:00 committed by GitHub
parent 638a59b8f5
commit ce424bf436
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 328 additions and 84 deletions

View File

@ -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
}
}
}
>

View File

@ -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]}

View File

@ -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'
)
}
}

View File

@ -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 (

View File

@ -90,7 +90,12 @@ describe(`${isListableSegment.name}`, () => {
const site: Pick<PlausibleSite, 'siteSegmentsAvailable'> = {
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)
})
})

View File

@ -145,7 +145,7 @@ export function KeybindHint({
return (
<kbd
className={classNames(
'rounded border border-gray-200 dark:border-gray-600 px-1.5 font-medium text-xs text-gray-400',
'rounded bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 px-1.5 font-medium text-xs text-gray-400',
className
)}
>

View File

@ -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'
)

View File

@ -1,5 +1,5 @@
import React from 'react'
export const MenuSeparator = () => (
<div className="my-1 border-gray-200 dark:border-gray-700 border-b" />
<div className="my-0.5 -mx-1 border-gray-200 dark:border-gray-700 border-b" />
)

View File

@ -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(

View File

@ -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

View File

@ -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'

View File

@ -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('') }))
)

View File

@ -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(<SegmentModal id={anySiteSegment.id} />, {
wrapper: (props) => (
<TestContextProviders
user={{ loggedIn: true, role: Role.editor, id: 1 }}
user={{
loggedIn: true,
role: Role.editor,
id: 1,
team: { identifier: null, hasConsolidatedView: false }
}}
preloaded={{
segments: [anySiteSegment]
}}

View File

@ -67,7 +67,8 @@ describe('parseSiteFromDataset', () => {
realtime: ['minute'],
year: ['day', 'week', 'month']
},
shared: false
shared: false,
isConsolidatedView: false
}
it('parses from dom string map correctly', () => {

View File

@ -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<string, Array<string>>,
shared: false
shared: false,
isConsolidatedView: false
}
export type PlausibleSite = typeof siteContextDefaultValue

View File

@ -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 }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className={className}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M22 12H2M12 22c5.714-5.442 5.714-14.558 0-20M12 22C6.286 16.558 6.286 7.442 12 2"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"
/>
</svg>
)
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 && (
<Keybind
key={user.team.identifier}
keyboardKey="0"
type="keydown"
handler={() => {
const url = getSwitchToSiteURL(currentSite, {
domain: user.team.identifier!
})
if (!url) {
closePopover()
} else {
closePopover()
window.location.assign(url)
}
}}
shouldIgnoreWhen={[isModifierPressed, isTyping]}
targetRef="document"
/>
)}
<BlurMenuButtonOnEscape targetRef={buttonRef} />
<Popover.Button
ref={buttonRef}
@ -119,12 +175,18 @@ export const SiteSwitcher = () => {
)}
title={currentSite.domain}
>
<Favicon
domain={currentSite.domain}
className="block h-4 w-4 mx-1"
/>
{currentSite.isConsolidatedView ? (
<GlobeIcon className="size-4 block mx-1 h-4 w-4 text-indigo-600 dark:text-white" />
) : (
<Favicon
domain={currentSite.domain}
className="block h-4 w-4 mx-1"
/>
)}
<span className={'truncate hidden sm:block sm:mr-1 lg:mr-0'}>
{currentSite.domain}
{currentSite.isConsolidatedView
? 'All sites'
: currentSite.domain}
</span>
<ChevronDownIcon className="hidden lg:block h-5 w-5 ml-2 dark:text-gray-100" />
</Popover.Button>
@ -140,18 +202,24 @@ export const SiteSwitcher = () => {
data-testid="sitemenu"
className={classNames(popover.panel.classNames.roundedSheet)}
>
{canSeeSiteSettings && (
<>
<div className="flex">
{canSeeViewAllSites && (
<a className={buttonLinkClassName} href={`/sites`}>
<ArrowLeftIcon className="size-4 mr-1.5" />
Back to sites
</a>
)}
{canSeeSiteSettings && (
<a
className={menuItemClassName}
className={buttonLinkClassName}
href={`/${encodeURIComponent(currentSite.domain)}/settings/general`}
>
<Cog8ToothIcon className="h-4 w-4 block mr-2" />
<span className="mr-auto">Site settings</span>
<Cog8ToothIcon className="size-4 mr-1.5" />
Site settings
</a>
<MenuSeparator />
</>
)}
)}
</div>
{(canSeeSiteSettings || canSeeViewAllSites) && <MenuSeparator />}
{sitesQuery.isLoading && (
<div className="px-3 py-2">
<div className="loading sm">
@ -167,6 +235,22 @@ export const SiteSwitcher = () => {
/>
</div>
)}
{user.team.hasConsolidatedView && user.team.identifier && (
<a
data-selected={currentSite.isConsolidatedView}
className={menuItemClassName}
href={
getSwitchToSiteURL(currentSite, {
domain: user.team.identifier
}) ?? '#'
}
onClick={() => closePopover()}
>
<GlobeIcon className="size-4 block mr-2 text-indigo-600 dark:text-white" />
<span className="truncate mr-auto">All sites</span>
<KeybindHint>0</KeybindHint>
</a>
)}
{!!sitesInDropdown &&
sitesInDropdown.map(({ domain }, index) => (
<a
@ -187,14 +271,6 @@ export const SiteSwitcher = () => {
)}
</a>
))}
{canSeeViewAllSites && (
<>
<MenuSeparator />
<a className={menuItemClassName} href={`/sites`}>
View all
</a>
</>
)}
</Popover.Panel>
</Transition>
</>

View File

@ -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'
)}
>

View File

@ -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

View File

@ -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 = ({
// <ThemeContextProvider> not interactive component, default value is suitable
<SiteContextProvider site={site}>
<UserContextProvider
user={user ?? { role: Role.editor, loggedIn: true, id: 1 }}
user={
user ?? {
role: Role.editor,
loggedIn: true,
id: 1,
team: { identifier: null, hasConsolidatedView: false }
}
}
>
<SegmentsContextProvider preloadedSegments={preloaded?.segments ?? []}>
<MemoryRouter

View File

@ -16,6 +16,20 @@ 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
end
@spec reset_if_enabled(Team.t()) :: :ok
def reset_if_enabled(%Team{} = team) do
case get(team) do

View File

@ -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

View File

@ -115,7 +115,7 @@ defmodule PlausibleWeb.Live.Sites do
<ul class="my-6 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<!-- Insert upgrade_card here -->
<.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}
/>
</div>
</div>
@ -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

View File

@ -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}
>
</div>
<div id="modal_root"></div>

View File

@ -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

View File

@ -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