Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion assets/js/dashboard/extra/exploration/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export const DIRECTION_OPTIONS: ExplorationDirectionOption[] = [

export const PAGE_FILTER_KEYS = ['page', 'entry_page', 'exit_page']

export const MAX_VISIBLE_CANDIDATES = 10
export const INITIAL_VISIBLE_CANDIDATES = 10
export const SHOW_MORE_INCREMENT = 10
export const MIN_GRID_COLUMNS = 3
56 changes: 53 additions & 3 deletions assets/js/dashboard/extra/exploration/exploration-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import {
} from '../../util/number-formatter'
import { CursorIcon, FolderIcon } from '../../components/icons'
import { popover } from '../../components/popover'
import { ChevronUpDownIcon } from '@heroicons/react/20/solid'
import {
ChevronUpDownIcon,
EllipsisHorizontalIcon
} from '@heroicons/react/20/solid'
import { FlagIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import { roundedPercentage } from './helpers'
import { journeyStepsEqual, JourneyStep, JourneySuggestion } from './journey'
import {
DIRECTION,
DIRECTION_OPTIONS,
MAX_VISIBLE_CANDIDATES,
INITIAL_VISIBLE_CANDIDATES,
SHOW_MORE_INCREMENT,
ExplorationDirection
} from './constants'

Expand Down Expand Up @@ -341,13 +345,40 @@ export function ExplorationColumn({
onFilterChange((e.target as HTMLInputElement).value)
)

// Track how many times the user has clicked "Show N more" for this column.
// Reset whenever the underlying results array reference changes so a new
// candidate list (filter change, journey change, etc.) starts collapsed.
const [expandCount, setExpandCount] = useState(0)
useEffect(() => {
setExpandCount(0)
}, [results])

// If the selected step lives beyond INITIAL_VISIBLE_CANDIDATES in a frozen
// column, make sure it is still visible by expanding the base window to
// include it. The user picked it from a list they could see, so it should
// remain visible after selection.
const selectedIndex =
selected && results.length > 0
? results.findIndex(({ step }) => journeyStepsEqual(step, selected))
: -1
const baseVisibleCount = Math.max(
INITIAL_VISIBLE_CANDIDATES,
selectedIndex >= 0 ? selectedIndex + 1 : 0
)
const visibleCount = Math.min(
results.length,
baseVisibleCount + expandCount * SHOW_MORE_INCREMENT
)
const remainingCount = Math.max(0, results.length - visibleCount)
const showMoreCount = Math.min(SHOW_MORE_INCREMENT, remainingCount)

// When a step is selected but there are no candidate results,
// synthesise a single-item list from the funnel data so
// the selected step is still rendered in the column.
const listItems =
selected && results.length === 0
? [{ step: selected, visitors: selectedVisitors ?? 0 }]
: results.slice(0, MAX_VISIBLE_CANDIDATES)
: results.slice(0, visibleCount)

const stepMaxVisitors = maxVisitors ?? results[0]?.visitors

Expand Down Expand Up @@ -427,6 +458,25 @@ export function ExplorationColumn({
onSelect={onSelectHandler}
/>
))}
{showMoreCount > 0 && (
<li data-testid="exploration-row">
<button
onClick={() => setExpandCount((c) => c + 1)}
className="group w-full text-sm rounded-sm hover:bg-gray-100/60 dark:hover:bg-gray-850 focus:outline-none"
>
<div
className={`flex items-center justify-between gap-2 px-2 py-1.5 ${
selected
? 'text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400'
: 'text-gray-600 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-gray-100'
}`}
>
<span>{`Show ${showMoreCount} more`}</span>
<EllipsisHorizontalIcon className="size-4 shrink-0" />
</div>
</button>
</li>
)}
</ul>
)}
</div>
Expand Down
126 changes: 126 additions & 0 deletions e2e/tests/dashboard/exploration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1387,3 +1387,129 @@ test('render various types of entries', async ({ page, request }) => {
await firstColumn.getByTestId('exploration-row').nth(6).locator('svg').hover()
await expect(page.getByRole('tooltip')).toHaveText(/Goal/)
})

test('load more suggestions', async ({ page, request }) => {
const report = getReport(page)
const explorationTabButton = getExplorationTabButton(report)
const { domain } = await setupSite({ page, request })

const events1 = [...Array(25).keys()].map((i) => {
return {
name: 'pageview',
pathname: `/pageone${String(i).padStart(2, '0')}`
}
})

const events2 = [...Array(25).keys()].map((i) => {
return {
name: 'pageview',
pathname: `/pagetwo${String(i).padStart(2, '0')}`
}
})

await populateStats({
request,
domain,
events: events1.concat(events2)
})

await page.goto('/' + domain, { waitUntil: 'commit' })

await explorationTabButton.scrollIntoViewIfNeeded()
await explorationTabButton.click()

await expect(report.getByTestId('exploration-title')).toHaveText(
'Explore user journeys'
)

await expect(
report.getByTestId('exploration-direction-forward')
).toBeVisible()

const firstColumn = report.getByTestId('exploration-column-0')

const columnRow = (i: number) =>
firstColumn.getByTestId('exploration-row').nth(i)

const firstPage = [
'/pageone00',
'/pageone01',
'/pageone02',
'/pageone03',
'/pageone04',
'/pageone05',
'/pageone06',
'/pageone07',
'/pageone08',
'/pageone09'
]

const secondPage = [
'/pageone10',
'/pageone11',
'/pageone12',
'/pageone13',
'/pageone14',
'/pageone15',
'/pageone16',
'/pageone17',
'/pageone18',
'/pageone19'
]

await expect(
firstColumn.getByTestId('exploration-row').getByTestId('metric-label')
).toHaveText(firstPage)

await expect(columnRow(10)).toHaveText(/Show 10 more/)
await columnRow(10).click()

await expect(
firstColumn.getByTestId('exploration-row').getByTestId('metric-label')
).toHaveText(firstPage.concat(secondPage))

await expect(columnRow(20)).toHaveText(/Show 10 more/)

await test.step('reset state when suggestions change', async () => {
await firstColumn.getByPlaceholder('Search').fill('pagetwo')

const newFirstPage = [
'/pagetwo00',
'/pagetwo01',
'/pagetwo02',
'/pagetwo03',
'/pagetwo04',
'/pagetwo05',
'/pagetwo06',
'/pagetwo07',
'/pagetwo08',
'/pagetwo09'
]

const newSecondPage = [
'/pagetwo10',
'/pagetwo11',
'/pagetwo12',
'/pagetwo13',
'/pagetwo14',
'/pagetwo15',
'/pagetwo16',
'/pagetwo17',
'/pagetwo18',
'/pagetwo19'
]

await expect(
firstColumn.getByTestId('exploration-row').getByTestId('metric-label')
).toHaveText(newFirstPage)

await expect(columnRow(10)).toHaveText(/Show 10 more/)
await columnRow(10).click()

await expect(
firstColumn.getByTestId('exploration-row').getByTestId('metric-label')
).toHaveText(newFirstPage.concat(newSecondPage))

await expect(columnRow(20)).toHaveText(/Show 5 more/)
})
})
2 changes: 1 addition & 1 deletion extra/lib/plausible/stats/exploration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ defmodule Plausible.Stats.Exploration do
}

@max_steps 20
@max_candidates 20
@max_candidates 50

@next_steps_defaults [
search_term: "",
Expand Down
5 changes: 4 additions & 1 deletion lib/plausible_web/controllers/api/stats_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ defmodule PlausibleWeb.Api.StatsController do
end
end

@exploration_max_candidates 50

def exploration_next_with_funnel(conn, %{"journey" => steps} = params) do
site = conn.assigns.site
search_term = params["search_term"] || ""
Expand All @@ -225,7 +227,8 @@ defmodule PlausibleWeb.Api.StatsController do
Exploration.next_steps(site, query, journey,
search_term: search_term,
direction: direction,
include_wildcard?: include_wildcard?
include_wildcard?: include_wildcard?,
max_candidates: @exploration_max_candidates
),
funnel <- maybe_include_funnel(include_funnel?, query, journey, direction) do
json(conn, %{next: next_steps, funnel: funnel})
Expand Down
Loading