Skip to content

Commit b8484ea

Browse files
RobertJoonassanne-sanapatacnkkcodesmith-bot
authored
Verification to dashboard (#6516)
* make verification work in a banner * move custom url input into the banner * improve notice styles * expandable instructions * stop polling pageviews * rely on tracker_script_config object for installation_method * proper dismiss logic * [revert me] debugging UI scenarios * rename verification (component) to verification_banner * Refine verification banner UI and copy (#6525) * Refine verification banner UI and copy - Simplify failure CTAs: keep "Check again" as primary, add ghost "Review installation" (or "Try another URL" when a custom URL retry is offered); remove installation-guide, change-method, and view-snippet expandable logic - Render inline "verify your installation manually" / "review your installation" links inside recommendation text (offer_custom_url_input scenarios) via safe HTML helpers, avoiding HEEx whitespace pitfalls - Tighten diagnostics title and body copy - Move the "Setup pending" pill in the sites list to replace the percentage indicator; use the generic pill component - Extend the notice component with title_class override and a spinner slot; swap success icon to solid check-circle * improve templating logic --------- Co-authored-by: Robert Joonas <robertjoonas16@gmail.com> * emails CTA banner * revert changes committed for debugging * fix test after rebase * fix CE tests * improve stats_controller_test.exs * E2E: named verification results + add genserver for mock scenarios * E2E: checks_mock module * E2E: plug in the ChecksMock module * E2E: put_verification_scenario endpoint * e2e test for verification success * get CI green * migration: add onboarding_status * update site schema with the new field * advance status to verification_succeeded * fix setup pending pill condition (/sites page) * further guard verification banner rendering in provisioning flow * fix verification re-triggering on refresh (review/domain_change) * add launch_delay option to MockScenarios * fix site switcher links + more e2e test coverage * new logic for email reports CTA * fix verification banner dismiss button showing through the dashboard options menu * validate named result exists in MockScenarios.put * move ChecksMock and MockScenarios into a single file, use :ets * move checks_mock.ex to test/support * different success message depending on the flow * fixup: make sure component goes into loading state instantly after clicking retry * stats_start_date -> ensure_stats_start_date ...and return the whole site struct. StatsController.stats action needs the whole site returned to known the "just updated" onboarding status when rendering the dashboard stats.html. * advance onboarding status from /sites page too * push_navigate -> redirect * changelog + change dashboard link text for CE on installation screen * credo and remove redundant else clause * fix installation_test (CE) * fix E2E test * remove runtime Mix.env call * do not persist verification params in browser history * Add workaround for CH query condition cache bug (#6562) * Use Blacksmith checkout with sticky-disk git mirror in Elixir CI (#6564) Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * Revert to actions/checkout in Elixir CI (git mirror regressed checkout time) (#6571) * Revert "Use Blacksmith checkout with sticky-disk git mirror in Elixir CI (#6564)" This reverts commit fd528f2. * Fix inert service container health checks in Elixir CI The postgres and clickhouse health-check options were nested under env, so Docker never received a health check and jobs could start before ClickHouse was listening, failing ecto.create with Mint.TransportError{reason: :closed}. Move options to the service level and point the ClickHouse check at the real HTTP port (8123 /ping) instead of the unused 8124. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * Give ClickHouse a 2m health-check start period ClickHouse on these runners can take over 50s to start listening during load spikes (the same slowness behind the Aug 3+ ecto.create failures). Without a start period the health check flips to unhealthy after ~50s and the runner kills the job before the server is ready; probe failures during the start period no longer count toward --health-retries. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * Probe ClickHouse health check via 127.0.0.1, not localhost The server fails to bind [::] in these containers (IPv6 disabled) and falls back to 0.0.0.0 only, while busybox wget resolves localhost to ::1 and does not retry the next address, so the probe got connection refused forever and every service init failed. Reproduced against clickhouse/clickhouse-server:25.11.5.8-alpine: localhost fails, 127.0.0.1 succeeds and the container reports healthy within seconds. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> --------- Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * fight with latency (Try another URL button) * fight latency (clicking Check again) Adjust the global btn-base styling to make the button appear "disabled", when phx-click-loading is set on it. * nom format e2e --------- Co-authored-by: Sanne de Vries <65487235+sanne-san@users.noreply.github.com> Co-authored-by: Artur Pata <artur.pata@gmail.com> Co-authored-by: Cenk Kücük <cenk@plausible.io> Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
1 parent abdcf50 commit b8484ea

67 files changed

Lines changed: 3385 additions & 1248 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file.
2121

2222
### Removed
2323

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

assets/css/app.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060
.btn-base {
6161
@apply whitespace-nowrap truncate inline-flex items-center justify-center
6262
gap-x-2 text-sm font-medium rounded-md cursor-pointer
63-
disabled:cursor-not-allowed;
63+
disabled:cursor-not-allowed
64+
phx-click-loading:cursor-not-allowed phx-click-loading:opacity-60;
6465
}
6566

6667
.btn-xs {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from 'react'
2+
import { render, screen, waitFor } from '@testing-library/react'
3+
import userEvent from '@testing-library/user-event'
4+
import { TestContextProviders } from '../../test-utils/app-context-providers'
5+
import { MockAPI } from '../../test-utils/mock-api'
6+
import { EmailReportsCTABanner } from './email-reports-cta-banner'
7+
8+
const domain = 'dummy.site'
9+
10+
let mockAPI: MockAPI
11+
12+
beforeAll(() => {
13+
mockAPI = new MockAPI().start()
14+
})
15+
16+
afterAll(() => {
17+
mockAPI.stop()
18+
})
19+
20+
beforeEach(() => {
21+
mockAPI.clear()
22+
})
23+
24+
function renderBanner(showEmailReportsCta: boolean) {
25+
render(<EmailReportsCTABanner />, {
26+
wrapper: (props) => (
27+
<TestContextProviders
28+
siteOptions={{ domain, showEmailReportsCta }}
29+
{...props}
30+
/>
31+
)
32+
})
33+
}
34+
35+
test('renders nothing when showEmailReportsCta is false', () => {
36+
renderBanner(false)
37+
38+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
39+
})
40+
41+
test('renders the banner when showEmailReportsCta is true', () => {
42+
renderBanner(true)
43+
44+
expect(screen.getByRole('alert')).toHaveTextContent(
45+
'Your first pageview has landed!'
46+
)
47+
})
48+
49+
test('dismissing fires the mutation and hides the banner', async () => {
50+
const putHandler = mockAPI.put(`/api/${domain}/complete-onboarding`, {})
51+
52+
renderBanner(true)
53+
54+
await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }))
55+
56+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
57+
await waitFor(() => expect(putHandler).toHaveBeenCalledTimes(1))
58+
})
59+
60+
test('the email reports link points at the settings page with cta_clicked=true', async () => {
61+
renderBanner(true)
62+
63+
const link = screen.getByRole('link', {
64+
name: /Get weekly traffic reports by email/
65+
})
66+
67+
expect(link).toHaveAttribute(
68+
'href',
69+
`/${domain}/settings/email-reports?cta_clicked=true`
70+
)
71+
})
72+
73+
test('still hides the banner (and does not throw) when the mutation fails', async () => {
74+
mockAPI.put(`/api/${domain}/complete-onboarding`, () =>
75+
Promise.resolve({
76+
ok: false,
77+
status: 401,
78+
json: async () => ({ error: 'unauthorized' })
79+
} as Response)
80+
)
81+
82+
renderBanner(true)
83+
84+
await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }))
85+
86+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
87+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, { useState } from 'react'
2+
import { XMarkIcon } from '@heroicons/react/24/outline'
3+
import { useSiteContext } from './site-context'
4+
import * as api from './api'
5+
6+
// CTA for configuring weekly email reports. It displays when the
7+
// dashboard is loaded for the very first time, having actual data
8+
// (i.e. site.onboarding_status == "first_pageview"). The CTA will
9+
// remain visible until either:
10+
//
11+
// 1) it's dismissed by any site member that sees it --
12+
// Notifies the backend to advance the site onboarding status via
13+
// a HTTP request.
14+
//
15+
// 2) the CTA is clicked by any site member that sees it --
16+
// The link to /:domain/settings/email-reports includes a query
17+
// parameter telling the controller action to advance the site's
18+
// onboarding status.
19+
export function EmailReportsCTABanner() {
20+
const site = useSiteContext()
21+
const [visible, setVisible] = useState(site.showEmailReportsCta)
22+
23+
if (!visible) {
24+
return null
25+
}
26+
27+
function dismiss() {
28+
setVisible(false)
29+
30+
api
31+
.mutation(`/api/${encodeURIComponent(site.domain)}/complete-onboarding`, {
32+
method: 'PUT',
33+
body: {}
34+
})
35+
.catch((error) => {
36+
if (!(error instanceof api.ApiError)) {
37+
throw error
38+
}
39+
})
40+
}
41+
42+
return (
43+
<div
44+
role="alert"
45+
className="text-md relative mb-4 rounded-md bg-indigo-100/60 p-4 text-center font-medium dark:bg-indigo-900/40"
46+
>
47+
<button
48+
type="button"
49+
aria-label="Dismiss"
50+
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"
51+
onClick={dismiss}
52+
>
53+
<XMarkIcon className="size-4" />
54+
</button>
55+
<span className="mr-1 text-base">🎉</span>
56+
<span className="text-gray-900 dark:text-gray-100">
57+
Your first pageview has landed!
58+
</span>{' '}
59+
<a
60+
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"
61+
href={`/${encodeURIComponent(site.domain)}/settings/email-reports?cta_clicked=true`}
62+
onClick={() => setVisible(false)}
63+
>
64+
Get weekly traffic reports by email →
65+
</a>
66+
</div>
67+
)
68+
}

assets/js/dashboard/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { isRealTimeDashboard } from './util/filters'
1111
import { GraphIntervalProvider } from './stats/graph/graph-interval-context'
1212
import { ImportsIncludedProvider } from './stats/graph/imports-included-context'
1313
import { CurrentVisitorsProvider } from './current-visitors-context'
14+
import { VerificationLiveViewPortal } from './verification/portal'
15+
import { EmailReportsCTABanner } from './email-reports-cta-banner'
1416

1517
function DashboardStats({
1618
importedDataInView,
@@ -21,7 +23,11 @@ function DashboardStats({
2123
}) {
2224
return (
2325
<>
24-
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
26+
<div className="col-span-full">
27+
<EmailReportsCTABanner />
28+
<VerificationLiveViewPortal />
29+
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
30+
</div>
2531
<Sources />
2632
<Pages />
2733
<Locations />

assets/js/dashboard/nav-menu/top-bar.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,29 @@ test('user can open and close site switcher', async () => {
7575
expect(screen.queryAllByRole('menuitem')).toEqual([])
7676
})
7777

78+
test('site switcher links to a site needing verification with verify_installation and flow params', async () => {
79+
mockAPI.get('/api/sites', {
80+
data: [
81+
{ domain, needs_verification: false },
82+
{ domain: 'example.com', needs_verification: true }
83+
]
84+
})
85+
86+
render(<TopBar showCurrentVisitors={false} />, {
87+
wrapper: (props) => (
88+
<TestContextProviders siteOptions={{ domain }} {...props} />
89+
)
90+
})
91+
92+
const toggleSiteSwitcher = screen.getByRole('button', { name: domain })
93+
await userEvent.click(toggleSiteSwitcher)
94+
95+
expect(screen.getByRole('link', { name: /example\.com/ })).toHaveAttribute(
96+
'href',
97+
'/example.com?verify_installation=true&flow=provisioning'
98+
)
99+
})
100+
78101
test('user can open and close filters dropdown', async () => {
79102
render(<TopBar showCurrentVisitors={false} />, {
80103
wrapper: (props) => (

assets/js/dashboard/site-context.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ describe('parseSiteFromDataset', () => {
6262
isDbip: false,
6363
flags: {},
6464
shared: false,
65-
isConsolidatedView: false
65+
isConsolidatedView: false,
66+
showEmailReportsCta: false
6667
}
6768

6869
it('parses from dom string map correctly', () => {

assets/js/dashboard/site-context.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export function parseSiteFromDataset(dataset: DOMStringMap): PlausibleSite {
2828
isDbip: dataset.isDbip === 'true',
2929
flags: JSON.parse(dataset.flags!),
3030
shared: !!dataset.sharedLinkAuth,
31-
isConsolidatedView: dataset.isConsolidatedView === 'true'
31+
isConsolidatedView: dataset.isConsolidatedView === 'true',
32+
showEmailReportsCta: dataset.showEmailReportsCta === 'true'
3233
}
3334
}
3435

@@ -62,7 +63,8 @@ export const siteContextDefaultValue = {
6263
isDbip: false,
6364
flags: {} as FeatureFlags,
6465
shared: false,
65-
isConsolidatedView: false
66+
isConsolidatedView: false,
67+
showEmailReportsCta: false
6668
}
6769

6870
export type PlausibleSite = typeof siteContextDefaultValue

assets/js/dashboard/site-switcher.tsx

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,17 @@ const buttonLinkClassName = classNames(
5858

5959
const getSwitchToSiteURL = (
6060
currentSite: PlausibleSite,
61-
site: { domain: string }
61+
site: { domain: string; needsVerification?: boolean }
6262
): string | null => {
6363
// Prevents reloading the page when the current site is selected
6464
if (currentSite.domain === site.domain) {
6565
return null
6666
}
67-
return `/${encodeURIComponent(site.domain)}`
67+
const url = `/${encodeURIComponent(site.domain)}`
68+
69+
return site.needsVerification
70+
? `${url}?verify_installation=true&flow=provisioning`
71+
: url
6872
}
6973

7074
const SiteSwitcherStatic = () => {
@@ -96,7 +100,9 @@ export const SiteSwitcher = () => {
96100
const sitesQuery = useQuery({
97101
enabled: user.loggedIn,
98102
queryKey: ['sites'],
99-
queryFn: async (): Promise<{ data: Array<{ domain: string }> }> => {
103+
queryFn: async (): Promise<{
104+
data: Array<{ domain: string; needs_verification: boolean }>
105+
}> => {
100106
const response = await get('/api/sites')
101107
return response
102108
},
@@ -121,24 +127,29 @@ export const SiteSwitcher = () => {
121127
<>
122128
{!!dashboardRouteMatch &&
123129
!modal &&
124-
sitesQuery.data?.data.slice(0, 8).map(({ domain }, index) => (
125-
<Keybind
126-
key={domain}
127-
keyboardKey={`${index + 1}`}
128-
type="keydown"
129-
handler={() => {
130-
const url = getSwitchToSiteURL(currentSite, { domain })
131-
if (!url) {
132-
closePopover()
133-
} else {
134-
closePopover()
135-
window.location.assign(url)
136-
}
137-
}}
138-
shouldIgnoreWhen={[isModifierPressed, isTyping]}
139-
targetRef="document"
140-
/>
141-
))}
130+
sitesQuery.data?.data
131+
.slice(0, 8)
132+
.map(({ domain, needs_verification }, index) => (
133+
<Keybind
134+
key={domain}
135+
keyboardKey={`${index + 1}`}
136+
type="keydown"
137+
handler={() => {
138+
const url = getSwitchToSiteURL(currentSite, {
139+
domain,
140+
needsVerification: needs_verification
141+
})
142+
if (!url) {
143+
closePopover()
144+
} else {
145+
closePopover()
146+
window.location.assign(url)
147+
}
148+
}}
149+
shouldIgnoreWhen={[isModifierPressed, isTyping]}
150+
targetRef="document"
151+
/>
152+
))}
142153

143154
{!!dashboardRouteMatch &&
144155
!modal &&
@@ -253,12 +264,17 @@ export const SiteSwitcher = () => {
253264
</a>
254265
)}
255266
{!!sitesInDropdown &&
256-
sitesInDropdown.map(({ domain }, index) => (
267+
sitesInDropdown.map(({ domain, needs_verification }, index) => (
257268
<a
258269
data-selected={currentSite.domain === domain}
259270
key={domain}
260271
className={menuItemClassName}
261-
href={getSwitchToSiteURL(currentSite, { domain }) ?? '#'}
272+
href={
273+
getSwitchToSiteURL(currentSite, {
274+
domain,
275+
needsVerification: needs_verification
276+
}) ?? '#'
277+
}
262278
onClick={
263279
currentSite.domain === domain
264280
? () => closePopover()

assets/js/dashboard/stats/graph/visitor-graph.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export default function VisitorGraph({
113113
!showFullLoader
114114

115115
return (
116-
<div className="col-span-full relative w-full bg-white rounded-md shadow-sm dark:bg-gray-900">
116+
<div className="relative w-full bg-white rounded-md shadow-sm dark:bg-gray-900">
117117
<>
118118
<div
119119
id="top-stats-container"

0 commit comments

Comments
 (0)