Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e4143f1
make verification work in a banner
RobertJoonas Jul 14, 2026
6611ad4
move custom url input into the banner
RobertJoonas Jul 15, 2026
9f3c567
improve notice styles
RobertJoonas Jul 15, 2026
894bcd5
expandable instructions
RobertJoonas Jul 16, 2026
a5b23b3
stop polling pageviews
RobertJoonas Jul 16, 2026
37f8951
rely on tracker_script_config object for installation_method
RobertJoonas Jul 16, 2026
dc490f7
proper dismiss logic
RobertJoonas Jul 20, 2026
b41381e
[revert me] debugging UI scenarios
RobertJoonas Jul 20, 2026
e170662
rename verification (component) to verification_banner
RobertJoonas Jul 20, 2026
a56dabd
Refine verification banner UI and copy (#6525)
sanne-san Jul 22, 2026
e6bd40c
emails CTA banner
RobertJoonas Jul 23, 2026
dde16fc
revert changes committed for debugging
RobertJoonas Jul 23, 2026
2494403
fix test after rebase
RobertJoonas Jul 23, 2026
c797cc0
fix CE tests
RobertJoonas Jul 23, 2026
f03da3a
improve stats_controller_test.exs
RobertJoonas Jul 23, 2026
f11d0c4
E2E: named verification results + add genserver for mock scenarios
RobertJoonas Jul 23, 2026
d2d969c
E2E: checks_mock module
RobertJoonas Jul 23, 2026
6dc97b7
E2E: plug in the ChecksMock module
RobertJoonas Jul 27, 2026
f67e7df
E2E: put_verification_scenario endpoint
RobertJoonas Jul 27, 2026
b52e667
e2e test for verification success
RobertJoonas Jul 27, 2026
3836bd8
get CI green
RobertJoonas Jul 27, 2026
eb517fd
migration: add onboarding_status
RobertJoonas Jul 27, 2026
530ffd2
update site schema with the new field
RobertJoonas Jul 27, 2026
d1f6130
advance status to verification_succeeded
RobertJoonas Jul 27, 2026
222a85a
fix setup pending pill condition (/sites page)
RobertJoonas Jul 27, 2026
69f90e0
further guard verification banner rendering in provisioning flow
RobertJoonas Jul 27, 2026
4f85806
fix verification re-triggering on refresh (review/domain_change)
RobertJoonas Jul 28, 2026
fdfa0b2
add launch_delay option to MockScenarios
RobertJoonas Jul 28, 2026
766d08d
fix site switcher links + more e2e test coverage
RobertJoonas Jul 28, 2026
a7293cc
new logic for email reports CTA
RobertJoonas Jul 28, 2026
5a4eff5
fix verification banner dismiss button showing through the dashboard …
RobertJoonas Jul 28, 2026
bddfec9
validate named result exists in MockScenarios.put
RobertJoonas Jul 28, 2026
f638a19
move ChecksMock and MockScenarios into a single file, use :ets
RobertJoonas Jul 28, 2026
3126a39
move checks_mock.ex to test/support
RobertJoonas Jul 28, 2026
0cb9ea2
different success message depending on the flow
RobertJoonas Jul 28, 2026
25ece8a
fixup: make sure component goes into loading state instantly after cl…
RobertJoonas Jul 29, 2026
9fc6b5f
stats_start_date -> ensure_stats_start_date
RobertJoonas Jul 29, 2026
3fd5593
advance onboarding status from /sites page too
RobertJoonas Jul 29, 2026
1131181
push_navigate -> redirect
RobertJoonas Jul 29, 2026
1e9e352
changelog + change dashboard link text for CE on installation screen
RobertJoonas Jul 29, 2026
63331fa
credo and remove redundant else clause
RobertJoonas Jul 30, 2026
5a9d159
fix installation_test (CE)
RobertJoonas Jul 30, 2026
3e8c4b8
fix E2E test
RobertJoonas Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file.

### Removed

- Removed the intermediate "Awaiting your first pageview" screen after site created. User is now taken straight to the dashboard from the installation instructions page.
- Removed the standalone team switcher page; team switching is now done from the topbar dropdown only
- Removed `Bamboo.SMTPAdapter` from supported e-mail adapters; the library is no longer in active developments and does not compile under Elixir 1.20+

Expand Down
87 changes: 87 additions & 0 deletions assets/js/dashboard/email-reports-cta-banner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TestContextProviders } from '../../test-utils/app-context-providers'
import { MockAPI } from '../../test-utils/mock-api'
import { EmailReportsCTABanner } from './email-reports-cta-banner'

const domain = 'dummy.site'

let mockAPI: MockAPI

beforeAll(() => {
mockAPI = new MockAPI().start()
})

afterAll(() => {
mockAPI.stop()
})

beforeEach(() => {
mockAPI.clear()
})

function renderBanner(showEmailReportsCta: boolean) {
render(<EmailReportsCTABanner />, {
wrapper: (props) => (
<TestContextProviders
siteOptions={{ domain, showEmailReportsCta }}
{...props}
/>
)
})
}

test('renders nothing when showEmailReportsCta is false', () => {
renderBanner(false)

expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})

test('renders the banner when showEmailReportsCta is true', () => {
renderBanner(true)

expect(screen.getByRole('alert')).toHaveTextContent(
'Your first pageview has landed!'
)
})

test('dismissing fires the mutation and hides the banner', async () => {
const putHandler = mockAPI.put(`/api/${domain}/complete-onboarding`, {})

renderBanner(true)

await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }))

expect(screen.queryByRole('alert')).not.toBeInTheDocument()
await waitFor(() => expect(putHandler).toHaveBeenCalledTimes(1))
})

test('the email reports link points at the settings page with cta_clicked=true', async () => {
renderBanner(true)

const link = screen.getByRole('link', {
name: /Get weekly traffic reports by email/
})

expect(link).toHaveAttribute(
'href',
`/${domain}/settings/email-reports?cta_clicked=true`
)
})

test('still hides the banner (and does not throw) when the mutation fails', async () => {
mockAPI.put(`/api/${domain}/complete-onboarding`, () =>
Promise.resolve({
ok: false,
status: 401,
json: async () => ({ error: 'unauthorized' })
} as Response)
)

renderBanner(true)

await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }))

expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})
68 changes: 68 additions & 0 deletions assets/js/dashboard/email-reports-cta-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useState } from 'react'
import { XMarkIcon } from '@heroicons/react/24/outline'
import { useSiteContext } from './site-context'
import * as api from './api'

// CTA for configuring weekly email reports. It displays when the
// dashboard is loaded for the very first time, having actual data
// (i.e. site.onboarding_status == "first_pageview"). The CTA will
// remain visible until either:
//
// 1) it's dismissed by any site member that sees it --
// Notifies the backend to advance the site onboarding status via
// a HTTP request.
//
// 2) the CTA is clicked by any site member that sees it --
// The link to /:domain/settings/email-reports includes a query
// parameter telling the controller action to advance the site's
// onboarding status.
export function EmailReportsCTABanner() {
const site = useSiteContext()
const [visible, setVisible] = useState(site.showEmailReportsCta)

if (!visible) {
return null
}

function dismiss() {
setVisible(false)

api
.mutation(`/api/${encodeURIComponent(site.domain)}/complete-onboarding`, {
method: 'PUT',
body: {}
})
.catch((error) => {
if (!(error instanceof api.ApiError)) {
throw error
}
})
}

return (
<div
role="alert"
className="text-md relative mb-4 rounded-md bg-indigo-100/60 p-4 text-center font-medium dark:bg-indigo-900/40"
>
<button
type="button"
aria-label="Dismiss"
className="absolute right-2 top-2 z-10 rounded p-1 text-gray-800 hover:text-gray-600 dark:text-gray-100/60 dark:hover:text-gray-100/70"
onClick={dismiss}
>
<XMarkIcon className="size-4" />
</button>
<span className="mr-1 text-base">🎉</span>
<span className="text-gray-900 dark:text-gray-100">
Your first pageview has landed!
</span>{' '}
<a
className="plausible-event-name=Weekly+Email+Note+Click text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-400 transition-colors duration-150"
href={`/${encodeURIComponent(site.domain)}/settings/email-reports?cta_clicked=true`}
onClick={() => setVisible(false)}
>
Get weekly traffic reports by email →
</a>
</div>
)
}
8 changes: 7 additions & 1 deletion assets/js/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { isRealTimeDashboard } from './util/filters'
import { GraphIntervalProvider } from './stats/graph/graph-interval-context'
import { ImportsIncludedProvider } from './stats/graph/imports-included-context'
import { CurrentVisitorsProvider } from './current-visitors-context'
import { VerificationLiveViewPortal } from './verification/portal'
import { EmailReportsCTABanner } from './email-reports-cta-banner'

function DashboardStats({
importedDataInView,
Expand All @@ -21,7 +23,11 @@ function DashboardStats({
}) {
return (
<>
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
<div className="col-span-full">
<EmailReportsCTABanner />
<VerificationLiveViewPortal />
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
</div>
<Sources />
<Pages />
<Locations />
Expand Down
23 changes: 23 additions & 0 deletions assets/js/dashboard/nav-menu/top-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,29 @@ test('user can open and close site switcher', async () => {
expect(screen.queryAllByRole('menuitem')).toEqual([])
})

test('site switcher links to a site needing verification with verify_installation and flow params', async () => {
mockAPI.get('/api/sites', {
data: [
{ domain, needs_verification: false },
{ domain: 'example.com', needs_verification: true }
]
})

render(<TopBar showCurrentVisitors={false} />, {
wrapper: (props) => (
<TestContextProviders siteOptions={{ domain }} {...props} />
)
})

const toggleSiteSwitcher = screen.getByRole('button', { name: domain })
await userEvent.click(toggleSiteSwitcher)

expect(screen.getByRole('link', { name: /example\.com/ })).toHaveAttribute(
'href',
'/example.com?verify_installation=true&flow=provisioning'
)
})

test('user can open and close filters dropdown', async () => {
render(<TopBar showCurrentVisitors={false} />, {
wrapper: (props) => (
Expand Down
3 changes: 2 additions & 1 deletion assets/js/dashboard/site-context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ describe('parseSiteFromDataset', () => {
isDbip: false,
flags: {},
shared: false,
isConsolidatedView: false
isConsolidatedView: false,
showEmailReportsCta: false
}

it('parses from dom string map correctly', () => {
Expand Down
6 changes: 4 additions & 2 deletions assets/js/dashboard/site-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export function parseSiteFromDataset(dataset: DOMStringMap): PlausibleSite {
isDbip: dataset.isDbip === 'true',
flags: JSON.parse(dataset.flags!),
shared: !!dataset.sharedLinkAuth,
isConsolidatedView: dataset.isConsolidatedView === 'true'
isConsolidatedView: dataset.isConsolidatedView === 'true',
showEmailReportsCta: dataset.showEmailReportsCta === 'true'
}
}

Expand Down Expand Up @@ -62,7 +63,8 @@ export const siteContextDefaultValue = {
isDbip: false,
flags: {} as FeatureFlags,
shared: false,
isConsolidatedView: false
isConsolidatedView: false,
showEmailReportsCta: false
}

export type PlausibleSite = typeof siteContextDefaultValue
Expand Down
62 changes: 39 additions & 23 deletions assets/js/dashboard/site-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,17 @@ const buttonLinkClassName = classNames(

const getSwitchToSiteURL = (
currentSite: PlausibleSite,
site: { domain: string }
site: { domain: string; needsVerification?: boolean }
): string | null => {
// Prevents reloading the page when the current site is selected
if (currentSite.domain === site.domain) {
return null
}
return `/${encodeURIComponent(site.domain)}`
const url = `/${encodeURIComponent(site.domain)}`

return site.needsVerification
? `${url}?verify_installation=true&flow=provisioning`
: url
}

const SiteSwitcherStatic = () => {
Expand Down Expand Up @@ -96,7 +100,9 @@ export const SiteSwitcher = () => {
const sitesQuery = useQuery({
enabled: user.loggedIn,
queryKey: ['sites'],
queryFn: async (): Promise<{ data: Array<{ domain: string }> }> => {
queryFn: async (): Promise<{
data: Array<{ domain: string; needs_verification: boolean }>
}> => {
const response = await get('/api/sites')
return response
},
Expand All @@ -121,24 +127,29 @@ export const SiteSwitcher = () => {
<>
{!!dashboardRouteMatch &&
!modal &&
sitesQuery.data?.data.slice(0, 8).map(({ domain }, index) => (
<Keybind
key={domain}
keyboardKey={`${index + 1}`}
type="keydown"
handler={() => {
const url = getSwitchToSiteURL(currentSite, { domain })
if (!url) {
closePopover()
} else {
closePopover()
window.location.assign(url)
}
}}
shouldIgnoreWhen={[isModifierPressed, isTyping]}
targetRef="document"
/>
))}
sitesQuery.data?.data
.slice(0, 8)
.map(({ domain, needs_verification }, index) => (
<Keybind
key={domain}
keyboardKey={`${index + 1}`}
type="keydown"
handler={() => {
const url = getSwitchToSiteURL(currentSite, {
domain,
needsVerification: needs_verification
})
if (!url) {
closePopover()
} else {
closePopover()
window.location.assign(url)
}
}}
shouldIgnoreWhen={[isModifierPressed, isTyping]}
targetRef="document"
/>
))}

{!!dashboardRouteMatch &&
!modal &&
Expand Down Expand Up @@ -253,12 +264,17 @@ export const SiteSwitcher = () => {
</a>
)}
{!!sitesInDropdown &&
sitesInDropdown.map(({ domain }, index) => (
sitesInDropdown.map(({ domain, needs_verification }, index) => (
<a
data-selected={currentSite.domain === domain}
key={domain}
className={menuItemClassName}
href={getSwitchToSiteURL(currentSite, { domain }) ?? '#'}
href={
getSwitchToSiteURL(currentSite, {
domain,
needsVerification: needs_verification
}) ?? '#'
}
onClick={
currentSite.domain === domain
? () => closePopover()
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/stats/graph/visitor-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function VisitorGraph({
!showFullLoader

return (
<div className="col-span-full relative w-full bg-white rounded-md shadow-sm dark:bg-gray-900">
<div className="relative w-full bg-white rounded-md shadow-sm dark:bg-gray-900">
<>
<div
id="top-stats-container"
Expand Down
Loading
Loading