Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
95fa2c3
transform pages_test
RobertJoonas Mar 19, 2026
0c59aed
make total_visitors an official metric
RobertJoonas Mar 19, 2026
eaf2c59
Dashboard.QueryParser: add pagination and order_by
RobertJoonas Mar 19, 2026
5044c11
improve API v2 interface
RobertJoonas Apr 19, 2026
d4836d7
prepare StatsQuery for v2 breakdowns and add usePaginatedQueryAPI hook
RobertJoonas Apr 19, 2026
7b87388
create copies of current use-order-by.ts and tests
RobertJoonas Apr 19, 2026
c65713c
use-order-by v2
RobertJoonas Apr 19, 2026
0fd1131
Create legacy copies of breakdown components
RobertJoonas Apr 19, 2026
6078c20
add v2 (details) breakdown components
RobertJoonas Apr 19, 2026
854478d
rename breakdown-modal -> details-breakdown
RobertJoonas Apr 19, 2026
cbd784a
create legacy copy of list.tsx
RobertJoonas Apr 19, 2026
6e39eca
add v2 version of ListReport
RobertJoonas Apr 19, 2026
19d8f4f
rename list.tsx -> index-breakdown.tsx
RobertJoonas Apr 19, 2026
df4c79b
pages/entry-pages/exit-pages v2
RobertJoonas Apr 19, 2026
6daccaf
destructure apiState before passing into IndexBreakdownRenderer
RobertJoonas Apr 28, 2026
a9984bd
use staleTime in usePaginatedQueryAPI
RobertJoonas Apr 28, 2026
de6fff4
Another iteration of improving v2 breakdown components
RobertJoonas Apr 28, 2026
05828c2
fix IndexBreakdownRenderer
RobertJoonas Apr 28, 2026
d084c99
fix NPM tests and lint
RobertJoonas May 4, 2026
5cac715
mix format
RobertJoonas May 4, 2026
87b3c80
Fix E2E tests
RobertJoonas May 4, 2026
9197d23
distribute extra horizontal space evenly
RobertJoonas May 4, 2026
c03265d
fix (NPM) top stats test again
RobertJoonas May 4, 2026
f629618
fix pages_test.exs
RobertJoonas May 4, 2026
915b4ba
stop rows remaining active after hover due to tooltip
RobertJoonas May 4, 2026
1093252
remove legacy pages.js
RobertJoonas May 5, 2026
bc9fde1
include page index in rowKey
RobertJoonas May 5, 2026
3c4f8d4
test file suggestions
RobertJoonas May 5, 2026
064ac74
not_sortable -> sortable
RobertJoonas May 5, 2026
d19018e
give more width to dimension cells
RobertJoonas May 6, 2026
b948931
fix legacy breakdown table min-height
RobertJoonas May 6, 2026
e281972
make metric label test match its description
RobertJoonas May 6, 2026
7716c10
use QueryResultQuery type in MainGraphResponse type
RobertJoonas May 6, 2026
04e8502
dimensionLabel argument to useOrderBy (v2) instead of reportInfo
RobertJoonas May 6, 2026
1319a2e
remove unnecessary wrapper fn
RobertJoonas May 6, 2026
2f64b3a
remove unnecessary cast
RobertJoonas May 6, 2026
fb6a759
remove unnecessary anonymous function
RobertJoonas May 6, 2026
8fedeb8
Merge remote-tracking branch 'origin/master' into v2-dashboard-pages
RobertJoonas May 6, 2026
8493e9b
bring back pages_test as legacy (still used by CSV export)
RobertJoonas May 6, 2026
009d3f8
increase dimension cell width a bit more on desktop
RobertJoonas May 6, 2026
74adeb4
Revert "increase dimension cell width a bit more on desktop"
RobertJoonas May 6, 2026
fbcbd73
Merge remote-tracking branch 'origin/master' into v2-dashboard-pages
RobertJoonas May 6, 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
44 changes: 32 additions & 12 deletions assets/js/dashboard/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Metric } from '../types/query-api'
import { Metric } from './stats/metrics'
import { DashboardState } from './dashboard-state'
import { PlausibleSite } from './site-context'
import { StatsQuery } from './stats-query'
Expand All @@ -9,18 +9,38 @@ import * as url from './util/url'
let abortController = new AbortController()
let SHARED_LINK_AUTH: null | string = null

export type RevenueMetricValue = {
short: string
value: number
long: string
currency: string
}

export type MetricValue = null | number | RevenueMetricValue

export type QueryResultQuery = {
metrics: Metric[]
dimensions: string[]
date_range: [string, string]
comparison_date_range?: [string, string] | null
}

export type QueryResultMeta = {
metric_warnings?: Record<string, Record<string, string>>
imports_included?: boolean
imports_skip_reason?: string
}

export type QueryResultRow = {
metrics: Array<MetricValue>
dimensions: Array<string>
comparison?: { metrics: Array<number>; change: Array<number> }
}

export type QueryApiResponse = {
query: {
metrics: Metric[]
date_range: [string, string]
comparison_date_range: [string, string]
}
meta: Record<string, unknown>
results: {
metrics: Array<number>
dimensions: Array<string>
comparison: { metrics: Array<number>; change: Array<number> }
}[]
query: QueryResultQuery
meta: QueryResultMeta
results: QueryResultRow[]
}

export class ApiError extends Error {
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/components/sort-button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react'
import { cycleSortDirection, SortDirection } from '../hooks/use-order-by'
import { cycleSortDirection, SortDirection } from '../hooks/use-order-by-legacy'
import classNames from 'classnames'

export const SortButton = ({
Expand Down
216 changes: 216 additions & 0 deletions assets/js/dashboard/components/table-legacy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import classNames from 'classnames'
import React, { ReactNode } from 'react'
import { SortDirection } from '../hooks/use-order-by-legacy'
import { SortButton } from './sort-button'
import { Tooltip } from '../util/tooltip'

export type ColumnConfiguraton<T extends Record<string, unknown>> = {
/** Unique column ID, used for sorting purposes and to get the value of the cell using rowItem[key] */
key: keyof T
/** Column title */
label: string
/** If defined, the column is considered sortable. @see SortButton */
onSort?: () => void
sortDirection?: SortDirection
/** CSS class string. @example "w-24 md:w-32" */
width: string
/** Aligns column content. */
align?: 'left' | 'right'
/** A warning to be rendered as a tooltip for the column header */
metricWarning?: string
/**
* Function used to transform the value found at item[key] for the cell. Superseded by renderItem if present. @example 1120 => "1.1k"
*/
renderValue?: (item: T, isRowHovered?: boolean) => ReactNode
/** Function used to create richer cells */
renderItem?: (item: T) => ReactNode
}

export const TableHeaderCell = ({
children,
className,
align
}: {
children: ReactNode
className: string
align?: 'left' | 'right'
}) => {
return (
<th
data-testid="report-header"
className={classNames(
'p-2 text-xs font-semibold text-gray-500 dark:text-gray-400',
className
)}
align={align}
>
{children}
</th>
)
}

export const TableCell = ({
children,
className,
align
}: {
children: ReactNode
className: string
align?: 'left' | 'right'
}) => {
return (
<td
className={classNames(
'p-2 font-medium first:rounded-s-sm last:rounded-e-sm',
className
)}
align={align}
>
{children}
</td>
)
}

export const ItemRow = <T extends Record<string, string | number | ReactNode>>({
rowIndex,
pageIndex,
item,
columns,
tappedRowName,
onRowTap
}: {
rowIndex: number
pageIndex?: number
item: T
columns: ColumnConfiguraton<T>[]
tappedRowName?: string | null
onRowTap?: (rowName: string | null) => void
}) => {
const [isHovered, setIsHovered] = React.useState(false)

const rowName = (item as unknown as { name: string }).name
const isTapped = tappedRowName === rowName
const isRowActive = isHovered || isTapped

const handleRowClick = (e: React.MouseEvent) => {
if (window.innerWidth < 768 && !(e.target as HTMLElement).closest('a')) {
if (onRowTap) {
if (isTapped) {
onRowTap(null)
} else {
onRowTap(rowName)
}
}
}
}

return (
<tr
data-testid="report-row"
className="group text-sm dark:text-gray-200 md:cursor-default cursor-pointer"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={handleRowClick}
>
{columns.map(({ key, width, align, renderValue, renderItem }) => (
<TableCell
key={`${(pageIndex ?? null) === null ? '' : `page_${pageIndex}_`}row_${rowIndex}_${String(key)}`}
className={width}
align={align}
>
{renderItem
? renderItem(item)
: renderValue
? renderValue(item, isRowActive)
: (item[key] ?? '')}
</TableCell>
))}
</tr>
)
}

export const Table = <T extends Record<string, string | number | ReactNode>>({
data,
columns
}: {
columns: ColumnConfiguraton<T>[]
data: T[] | { pages: T[][] }
}) => {
const [tappedRowName, setTappedRowName] = React.useState<string | null>(null)

const renderColumnLabel = (column: ColumnConfiguraton<T>) => {
if (column.metricWarning) {
return (
<Tooltip
info={warningSpan(column.metricWarning)}
className="inline-block"
>
{column.label + ' *'}
</Tooltip>
)
} else {
return column.label
}
}

const warningSpan = (warning: string) => {
return (
<span className="text-xs font-normal whitespace-nowrap">
{'* ' + warning}
</span>
)
}

return (
<table className="border-collapse table-striped table-fixed w-max min-w-full">
<thead className="sticky top-0 bg-white dark:bg-gray-900 z-10">
<tr className="text-xs font-semibold text-gray-500 dark:text-gray-400">
{columns.map((column) => (
<TableHeaderCell
key={`header_${String(column.key)}`}
className={classNames('p-2', column.width)}
align={column.align}
>
{column.onSort ? (
<SortButton
toggleSort={column.onSort}
sortDirection={column.sortDirection ?? null}
>
{renderColumnLabel(column)}
</SortButton>
) : (
renderColumnLabel(column)
)}
</TableHeaderCell>
))}
</tr>
</thead>
<tbody>
{Array.isArray(data)
? data.map((item, rowIndex) => (
<ItemRow
item={item}
columns={columns}
rowIndex={rowIndex}
key={rowIndex}
tappedRowName={tappedRowName}
onRowTap={setTappedRowName}
/>
))
: data.pages.map((page, pageIndex) =>
page.map((item, rowIndex) => (
<ItemRow
item={item}
columns={columns}
rowIndex={rowIndex}
pageIndex={pageIndex}
key={`page_${pageIndex}_row_${rowIndex}`}
tappedRowName={tappedRowName}
onRowTap={setTappedRowName}
/>
))
)}
</tbody>
</table>
)
}
Loading
Loading