mirror of
https://github.com/plausible/analytics.git
synced 2024-11-25 07:06:11 +03:00
Improve sorting indicators, left-align tables, set column width by metric
This commit is contained in:
parent
57037fdb82
commit
bd80e910fc
@ -1,25 +1,48 @@
|
||||
/** @format */
|
||||
|
||||
import React, { ReactNode } from 'react'
|
||||
import { getSortDirectionIndicator, SortDirection } from '../hooks/use-order-by'
|
||||
import { SortDirection } from '../hooks/use-order-by'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export const SortButton = ({
|
||||
children,
|
||||
toggleSort,
|
||||
hint,
|
||||
sortDirection
|
||||
sortDirection,
|
||||
nextSortDirection
|
||||
}: {
|
||||
children: ReactNode
|
||||
toggleSort: () => void
|
||||
hint: string
|
||||
sortDirection: SortDirection | null
|
||||
nextSortDirection: SortDirection
|
||||
}) => {
|
||||
return (
|
||||
<button onClick={toggleSort} title={hint} className="hover:underline">
|
||||
{children}
|
||||
{sortDirection !== null && (
|
||||
<span> {getSortDirectionIndicator(sortDirection)}</span>
|
||||
<button
|
||||
onClick={toggleSort}
|
||||
title={hint}
|
||||
className={classNames(
|
||||
'group',
|
||||
'hover:underline',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<span
|
||||
className={classNames(
|
||||
'rounded inline-block h-4 w-4',
|
||||
'ml-1',
|
||||
{
|
||||
[SortDirection.asc]: 'rotate-180',
|
||||
[SortDirection.desc]: 'rotate-0'
|
||||
}[sortDirection ?? nextSortDirection],
|
||||
!sortDirection && 'opacity-0',
|
||||
!sortDirection && 'group-hover:opacity-100',
|
||||
sortDirection && 'group-hover:bg-gray-100 dark:group-hover:bg-gray-900',
|
||||
'transition',
|
||||
)}
|
||||
>
|
||||
↓
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
77
assets/js/dashboard/components/table.tsx
Normal file
77
assets/js/dashboard/components/table.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
/** @format */
|
||||
|
||||
import classNames from 'classnames'
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
export const TableHeaderCell = ({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: ReactNode
|
||||
className: string
|
||||
}) => {
|
||||
return (
|
||||
<th
|
||||
className={classNames(
|
||||
'px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
export const TableCell = ({
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
children: ReactNode
|
||||
className: string
|
||||
}) => {
|
||||
return (
|
||||
<td
|
||||
className={classNames(
|
||||
'px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
export const Table = <T extends Record<string, string | number | ReactNode>>({
|
||||
data,
|
||||
columns
|
||||
}: {
|
||||
columns: { accessor: keyof T; width: string; label: string }[]
|
||||
data: T[]
|
||||
}) => {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white border border-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<TableHeaderCell key={index} className={column.width}>
|
||||
{column.label}
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{data.map((item, itemIndex) => (
|
||||
<tr key={itemIndex}>
|
||||
{columns.map(({ accessor, width }, colIndex) => (
|
||||
<TableCell key={colIndex} className={width}>
|
||||
{item[accessor]}
|
||||
</TableCell>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -319,7 +319,7 @@ export default function Funnel({ funnelName, tabs }) {
|
||||
<Bar
|
||||
count={step.visitors}
|
||||
all={funnel.steps}
|
||||
bg={palette.smallBarClass}
|
||||
className={palette.smallBarClass}
|
||||
maxWidthDeduction={"5rem"}
|
||||
plot={'visitors'}
|
||||
>
|
||||
|
@ -1,84 +0,0 @@
|
||||
import { useEffect } from "react"
|
||||
import { useQueryClient, useInfiniteQuery } from "@tanstack/react-query"
|
||||
import * as api from "../api"
|
||||
|
||||
const LIMIT = 10 // FOR DEBUGGING
|
||||
|
||||
/**
|
||||
* A wrapper for the React Query library. Constructs the necessary options
|
||||
* (including pagination config) to pass into the `useInfiniteQuery` hook.
|
||||
*
|
||||
* ### Required props
|
||||
*
|
||||
* @param {Array} key - The key under which the global "query" instance will live.
|
||||
* Should be passed as a list of two elements - `[endpoint, { query }]`. The object
|
||||
* can also contain additional values (such as `search`) to be used by:
|
||||
* 1) React Query, to determine the uniqueness of the query instance
|
||||
* 2) the `getRequestParams` function to build the request params.
|
||||
*
|
||||
* @param {Function} getRequestParams - A function that takes the `key` prop as an
|
||||
* argument, and returns `[query, params]` which will be used by `queryFn` that
|
||||
* actually calls the API.
|
||||
*
|
||||
* ### Optional props
|
||||
*
|
||||
* @param {Function} [afterFetchData] - A function to call after data has been fetched.
|
||||
* Receives the API response as an argument.
|
||||
*
|
||||
* @param {Function} [afterFetchNextPage] - A function to call after the next page has
|
||||
* been fetched. Receives the API response as an argument.
|
||||
*/
|
||||
|
||||
export function useAPIClient(props) {
|
||||
const {key, getRequestParams, afterFetchData, afterFetchNextPage} = props
|
||||
const [endpoint] = key
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const queryFn = async ({ pageParam, queryKey }) => {
|
||||
const [query, params] = getRequestParams(queryKey)
|
||||
params.limit = LIMIT
|
||||
params.page = pageParam
|
||||
|
||||
const response = await api.get(endpoint, query, params)
|
||||
|
||||
if (pageParam === 1 && typeof afterFetchData === 'function') {
|
||||
afterFetchData(response)
|
||||
}
|
||||
|
||||
if (pageParam > 1 && typeof afterFetchNextPage === 'function') {
|
||||
afterFetchNextPage(response)
|
||||
}
|
||||
|
||||
return response.results
|
||||
}
|
||||
|
||||
// During the cleanup phase, make sure only the first page of results
|
||||
// is cached under any `queryKey` containing this endpoint.
|
||||
useEffect(() => {
|
||||
const key = [endpoint]
|
||||
return () => {
|
||||
queryClient.setQueriesData(key, (data) => {
|
||||
if (data?.pages?.length) {
|
||||
return {
|
||||
pages: data.pages.slice(0, 1),
|
||||
pageParams: data.pageParams.slice(0, 1),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [queryClient, endpoint])
|
||||
|
||||
const getNextPageParam = (lastPageResults, _, lastPageIndex) => {
|
||||
return lastPageResults.length === LIMIT ? lastPageIndex + 1 : null
|
||||
}
|
||||
const defaultInitialPageParam = 1
|
||||
const initialPageParam = props.initialPageParam === undefined ? defaultInitialPageParam : props.initialPageParam
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
getNextPageParam,
|
||||
initialPageParam,
|
||||
placeholderData: (previousData) => previousData,
|
||||
})
|
||||
}
|
102
assets/js/dashboard/hooks/api-client.ts
Normal file
102
assets/js/dashboard/hooks/api-client.ts
Normal file
@ -0,0 +1,102 @@
|
||||
/** @format */
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
useQueryClient,
|
||||
useInfiniteQuery,
|
||||
QueryFilters,
|
||||
} from '@tanstack/react-query'
|
||||
import * as api from '../api'
|
||||
import { DashboardQuery } from '../query';
|
||||
|
||||
const LIMIT = 10 // FOR DEBUGGING
|
||||
|
||||
/**
|
||||
* A wrapper for the React Query library. Constructs the necessary options
|
||||
* (including pagination config) to pass into the `useInfiniteQuery` hook.
|
||||
*
|
||||
* ### Required props
|
||||
*
|
||||
* @param {Array} key - The key under which the global "query" instance will live.
|
||||
* Should be passed as a list of two elements - `[endpoint, { query }]`. The object
|
||||
* can also contain additional values (such as `search`) to be used by:
|
||||
* 1) React Query, to determine the uniqueness of the query instance
|
||||
* 2) the `getRequestParams` function to build the request params.
|
||||
*
|
||||
* @param {Function} getRequestParams - A function that takes the `key` prop as an
|
||||
* argument, and returns `[query, params]` which will be used by `queryFn` that
|
||||
* actually calls the API.
|
||||
*
|
||||
* ### Optional props
|
||||
*
|
||||
* @param {Function} [afterFetchData] - A function to call after data has been fetched.
|
||||
* Receives the API response as an argument.
|
||||
*
|
||||
* @param {Function} [afterFetchNextPage] - A function to call after the next page has
|
||||
* been fetched. Receives the API response as an argument.
|
||||
*/
|
||||
|
||||
|
||||
type Endpoint = string;
|
||||
|
||||
type InfiniteQueryKey = [Endpoint, {query: DashboardQuery}]
|
||||
|
||||
export function useAPIClient<TResponse, TKey extends InfiniteQueryKey = InfiniteQueryKey>(props: {
|
||||
initialPageParam?: number
|
||||
key: TKey
|
||||
getRequestParams: (key: TKey) => [Record<string, unknown>, Record<string, unknown>]
|
||||
afterFetchData: (response: TResponse) => void
|
||||
afterFetchNextPage: (response: TResponse) => void
|
||||
}) {
|
||||
const { key, getRequestParams, afterFetchData, afterFetchNextPage } = props
|
||||
const [endpoint] = key
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// During the cleanup phase, make sure only the first page of results
|
||||
// is cached under any `queryKey` containing this endpoint.
|
||||
useEffect(() => {
|
||||
const queryKeyToClean = [endpoint] as QueryFilters
|
||||
return () => {
|
||||
queryClient.setQueriesData<{pages: TResponse[], pageParams: unknown[]}>(queryKeyToClean, (data) => {
|
||||
if (data?.pages?.length) {
|
||||
return {
|
||||
pages: data.pages.slice(0, 1),
|
||||
pageParams: data.pageParams.slice(0, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [queryClient, endpoint])
|
||||
|
||||
const defaultInitialPageParam = 1
|
||||
const initialPageParam =
|
||||
props.initialPageParam === undefined
|
||||
? defaultInitialPageParam
|
||||
: props.initialPageParam
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: async ({ pageParam, queryKey }) => {
|
||||
const [query, params] = getRequestParams(queryKey)
|
||||
params.limit = LIMIT
|
||||
params.page = pageParam
|
||||
|
||||
const response = await api.get(endpoint, query, params)
|
||||
|
||||
if (pageParam === 1 && typeof afterFetchData === 'function') {
|
||||
afterFetchData(response)
|
||||
}
|
||||
|
||||
if (pageParam > 1 && typeof afterFetchNextPage === 'function') {
|
||||
afterFetchNextPage(response)
|
||||
}
|
||||
|
||||
return response.results
|
||||
},
|
||||
getNextPageParam: (lastPageResults, _, lastPageIndex) => {
|
||||
return lastPageResults.length === LIMIT ? lastPageIndex + 1 : null
|
||||
},
|
||||
initialPageParam,
|
||||
placeholderData: (previousData) => previousData
|
||||
})
|
||||
}
|
@ -8,26 +8,30 @@ export enum SortDirection {
|
||||
desc = 'desc'
|
||||
}
|
||||
|
||||
type Order = [Metric['key'], SortDirection]
|
||||
export type Order = [Metric['key'], SortDirection]
|
||||
|
||||
export type OrderBy = Order[]
|
||||
|
||||
export const getSortDirectionIndicator = (
|
||||
sortDirection: SortDirection
|
||||
): string =>
|
||||
({ [SortDirection.asc]: '↑', [SortDirection.desc]: '↓' })[sortDirection]
|
||||
|
||||
export const getSortDirectionLabel = (sortDirection: SortDirection): string =>
|
||||
({
|
||||
[SortDirection.asc]: 'Sorted in ascending order',
|
||||
[SortDirection.desc]: 'Sorted in descending order'
|
||||
})[sortDirection]
|
||||
|
||||
export function useOrderBy({ metrics }: { metrics: Metric[] }) {
|
||||
export function useOrderBy({
|
||||
metrics,
|
||||
defaultOrderBy
|
||||
}: {
|
||||
metrics: Metric[]
|
||||
defaultOrderBy: OrderBy
|
||||
}) {
|
||||
const [orderBy, setOrderBy] = useState<OrderBy>([])
|
||||
const orderByDictionary = useMemo(
|
||||
() => Object.fromEntries(orderBy),
|
||||
[orderBy]
|
||||
() =>
|
||||
orderBy.length
|
||||
? Object.fromEntries(orderBy)
|
||||
: Object.fromEntries(defaultOrderBy),
|
||||
[orderBy, defaultOrderBy]
|
||||
)
|
||||
|
||||
const toggleSortByMetric = useCallback(
|
||||
@ -35,19 +39,24 @@ export function useOrderBy({ metrics }: { metrics: Metric[] }) {
|
||||
if (!metrics.find(({ key }) => key === metric.key)) {
|
||||
return
|
||||
}
|
||||
setOrderBy((currentOrderBy) => rearrangeOrderBy(currentOrderBy, metric))
|
||||
setOrderBy((currentOrderBy) => rearrangeOrderBy(currentOrderBy.length ? currentOrderBy : defaultOrderBy, metric))
|
||||
},
|
||||
[metrics]
|
||||
[metrics, defaultOrderBy]
|
||||
)
|
||||
|
||||
return { orderBy, orderByDictionary, toggleSortByMetric }
|
||||
return {
|
||||
orderBy: orderBy.length ? orderBy : defaultOrderBy,
|
||||
orderByDictionary,
|
||||
toggleSortByMetric
|
||||
}
|
||||
}
|
||||
|
||||
export function cycleSortDirection(
|
||||
currentSortDirection: SortDirection | null
|
||||
): { direction: SortDirection | null; hint: string } {
|
||||
): { direction: SortDirection; hint: string } {
|
||||
switch (currentSortDirection) {
|
||||
case null:
|
||||
case SortDirection.asc:
|
||||
return {
|
||||
direction: SortDirection.desc,
|
||||
hint: 'Press to sort column in descending order'
|
||||
@ -57,8 +66,8 @@ export function cycleSortDirection(
|
||||
direction: SortDirection.asc,
|
||||
hint: 'Press to sort column in ascending order'
|
||||
}
|
||||
case SortDirection.asc:
|
||||
return { direction: null, hint: 'Press to remove sorting from column' }
|
||||
// case SortDirection.asc:
|
||||
// return { direction: SortDirection, hint: 'Press to remove sorting from column' }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,22 @@
|
||||
/** @format */
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import React, { useState, useEffect, useRef, ReactNode } from 'react'
|
||||
|
||||
import { FilterLink } from '../reports/list'
|
||||
import { useQueryContext } from '../../query-context'
|
||||
import { useDebounce } from '../../custom-hooks'
|
||||
import { useAPIClient } from '../../hooks/api-client'
|
||||
import { rootRoute } from '../../router'
|
||||
import { cycleSortDirection, useOrderBy } from '../../hooks/use-order-by'
|
||||
import {
|
||||
cycleSortDirection,
|
||||
Order,
|
||||
OrderBy,
|
||||
useOrderBy
|
||||
} from '../../hooks/use-order-by'
|
||||
import { SortButton } from '../../components/sort-button'
|
||||
import { Metric } from '../reports/metrics'
|
||||
import { DashboardQuery } from '../../query'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export const MIN_HEIGHT_PX = 500
|
||||
|
||||
@ -76,7 +84,7 @@ export const MIN_HEIGHT_PX = 500
|
||||
|
||||
// * `afterFetchNextPage` - a function with the same behaviour as `afterFetchData`,
|
||||
// but will be called after a successful next page load in `fetchNextPage`.
|
||||
export default function BreakdownModal({
|
||||
export default function BreakdownModal<TListItem extends {name: string}>({
|
||||
reportInfo,
|
||||
metrics,
|
||||
renderIcon,
|
||||
@ -86,13 +94,29 @@ export default function BreakdownModal({
|
||||
afterFetchNextPage,
|
||||
addSearchFilter,
|
||||
getFilterInfo
|
||||
}: {
|
||||
reportInfo: {
|
||||
title: string
|
||||
endpoint: string
|
||||
dimensionLabel: string
|
||||
defaultOrderBy: Order
|
||||
}
|
||||
metrics: Metric[]
|
||||
renderIcon: (listItem: TListItem) => ReactNode
|
||||
getExternalLinkURL: (listItem: TListItem) => string
|
||||
searchEnabled?: boolean
|
||||
afterFetchData: () => void
|
||||
afterFetchNextPage: () => void
|
||||
addSearchFilter: (q: DashboardQuery, search: string) => Record<string, unknown>
|
||||
getFilterInfo: (listItem: TListItem) => void
|
||||
}) {
|
||||
const searchBoxRef = useRef(null)
|
||||
const searchBoxRef = useRef<HTMLInputElement>(null)
|
||||
const { query } = useQueryContext()
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const { orderBy, orderByDictionary, toggleSortByMetric } = useOrderBy({
|
||||
metrics
|
||||
metrics,
|
||||
defaultOrderBy: [reportInfo.defaultOrderBy]
|
||||
})
|
||||
|
||||
const {
|
||||
@ -102,12 +126,15 @@ export default function BreakdownModal({
|
||||
isFetchingNextPage,
|
||||
isFetching,
|
||||
isPending
|
||||
} = useAPIClient({
|
||||
} = useAPIClient<
|
||||
never,
|
||||
[string, { query: DashboardQuery; search: string; orderBy: OrderBy }]
|
||||
>({
|
||||
key: [reportInfo.endpoint, { query, search, orderBy }],
|
||||
getRequestParams: (key) => {
|
||||
const [_endpoint, { query, search }] = key
|
||||
|
||||
let queryWithSearchFilter = { ...query }
|
||||
let queryWithSearchFilter: Record<string, unknown> = { ...query }
|
||||
|
||||
if (searchEnabled && search !== '') {
|
||||
queryWithSearchFilter = addSearchFilter(query, search)
|
||||
@ -115,7 +142,10 @@ export default function BreakdownModal({
|
||||
|
||||
return [
|
||||
queryWithSearchFilter,
|
||||
{ detailed: true, order_by: JSON.stringify(orderBy) }
|
||||
{
|
||||
detailed: true,
|
||||
order_by: JSON.stringify(orderBy)
|
||||
}
|
||||
]
|
||||
},
|
||||
afterFetchData,
|
||||
@ -123,33 +153,32 @@ export default function BreakdownModal({
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchEnabled) {
|
||||
const searchBox = searchBoxRef.current
|
||||
if (!searchEnabled && searchBox) {
|
||||
return
|
||||
}
|
||||
|
||||
const searchBox = searchBoxRef.current
|
||||
|
||||
const handleKeyUp = (event) => {
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.target.blur()
|
||||
;(event.target as HTMLElement | undefined)?.blur()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
searchBox.addEventListener('keyup', handleKeyUp)
|
||||
searchBox?.addEventListener('keyup', handleKeyUp)
|
||||
|
||||
return () => {
|
||||
searchBox.removeEventListener('keyup', handleKeyUp)
|
||||
searchBox?.removeEventListener('keyup', handleKeyUp)
|
||||
}
|
||||
}, [searchEnabled])
|
||||
|
||||
function maybeRenderIcon(item) {
|
||||
function maybeRenderIcon(item: TListItem) {
|
||||
if (typeof renderIcon === 'function') {
|
||||
return renderIcon(item)
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderExternalLink(item) {
|
||||
function maybeRenderExternalLink(item: TListItem) {
|
||||
if (typeof getExternalLinkURL === 'function') {
|
||||
const linkUrl = getExternalLinkURL(item)
|
||||
|
||||
@ -177,20 +206,20 @@ export default function BreakdownModal({
|
||||
}
|
||||
}
|
||||
|
||||
function renderRow(item) {
|
||||
function renderRow(item: TListItem) {
|
||||
return (
|
||||
<tr className="text-sm dark:text-gray-200" key={item.name}>
|
||||
<td className="w-48 md:w-80 break-all p-2 flex items-center">
|
||||
{maybeRenderIcon(item)}
|
||||
<FilterLink path={rootRoute.path} filterInfo={getFilterInfo(item)}>
|
||||
<FilterLink path={rootRoute.path} filterInfo={getFilterInfo(item)} onClick={undefined} extraClass={undefined}>
|
||||
{item.name}
|
||||
</FilterLink>
|
||||
{maybeRenderExternalLink(item)}
|
||||
</td>
|
||||
{metrics.map((metric) => {
|
||||
return (
|
||||
<td key={metric.key} className="p-2 w-24 font-medium" align="right">
|
||||
{metric.renderValue(item[metric.key])}
|
||||
<td key={metric.key} className={classNames(metric.width, "p-2 font-medium")} align="left">
|
||||
{metric.renderValue(item[metric.key as keyof TListItem])}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
@ -226,7 +255,7 @@ export default function BreakdownModal({
|
||||
return (
|
||||
<div className="flex flex-col w-full my-4 items-center justify-center h-10">
|
||||
{!isFetching && (
|
||||
<button onClick={fetchNextPage} type="button" className="button">
|
||||
<button onClick={() => fetchNextPage()} type="button" className="button">
|
||||
Load more
|
||||
</button>
|
||||
)}
|
||||
@ -235,8 +264,12 @@ export default function BreakdownModal({
|
||||
)
|
||||
}
|
||||
|
||||
function handleInputChange(e) {
|
||||
setSearch(e.target.value)
|
||||
function handleInputChange(e: Event) {
|
||||
const element = e.target as HTMLInputElement | null;
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
setSearch(element.value)
|
||||
}
|
||||
|
||||
const debouncedHandleInputChange = useDebounce(handleInputChange)
|
||||
@ -261,7 +294,7 @@ export default function BreakdownModal({
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
className="p-2 w-48 md:w-80 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
|
||||
className="p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
|
||||
align="left"
|
||||
>
|
||||
{reportInfo.dimensionLabel}
|
||||
@ -271,12 +304,15 @@ export default function BreakdownModal({
|
||||
return (
|
||||
<th
|
||||
key={metric.key}
|
||||
className="p-2 w-24 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
|
||||
align="right"
|
||||
className={classNames(metric.width, "p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400")}
|
||||
align="left"
|
||||
>
|
||||
{metric.sortable ? (
|
||||
<SortButton
|
||||
sortDirection={orderByDictionary[metric.key] ?? null}
|
||||
nextSortDirection={cycleSortDirection(
|
||||
orderByDictionary[metric.key] ?? null
|
||||
).direction!}
|
||||
toggleSort={() => toggleSortByMetric(metric)}
|
||||
hint={
|
||||
cycleSortDirection(
|
@ -16,7 +16,8 @@ function ConversionsModal() {
|
||||
title: 'Goal Conversions',
|
||||
dimension: 'goal',
|
||||
endpoint: url.apiPath(site, '/conversions'),
|
||||
dimensionLabel: "Goal"
|
||||
dimensionLabel: "Goal",
|
||||
defaultOrderBy: []
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
|
@ -7,6 +7,7 @@ import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { browserIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
import { SortDirection } from "../../../hooks/use-order-by";
|
||||
|
||||
function BrowserVersionsModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -16,7 +17,8 @@ function BrowserVersionsModal() {
|
||||
title: 'Browser Versions',
|
||||
dimension: 'browser_version',
|
||||
endpoint: url.apiPath(site, '/browser-versions'),
|
||||
dimensionLabel: 'Browser version'
|
||||
dimensionLabel: 'Browser version',
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
|
@ -7,6 +7,7 @@ import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { browserIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
import { SortDirection } from "../../../hooks/use-order-by";
|
||||
|
||||
function BrowsersModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -16,7 +17,8 @@ function BrowsersModal() {
|
||||
title: 'Browsers',
|
||||
dimension: 'browser',
|
||||
endpoint: url.apiPath(site, '/browsers'),
|
||||
dimensionLabel: 'Browser'
|
||||
dimensionLabel: 'Browser',
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
|
@ -5,14 +5,14 @@ export default function chooseMetrics(query) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions', width: 'w-32' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors', width: 'w-36' }),
|
||||
metrics.createPercentage()
|
||||
]
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { osIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
import { SortDirection } from "../../../hooks/use-order-by";
|
||||
|
||||
function OperatingSystemVersionsModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -16,7 +17,8 @@ function OperatingSystemVersionsModal() {
|
||||
title: 'Operating System Versions',
|
||||
dimension: 'os_version',
|
||||
endpoint: url.apiPath(site, '/operating-system-versions'),
|
||||
dimensionLabel: 'Operating system version'
|
||||
dimensionLabel: 'Operating system version',
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
|
@ -7,6 +7,7 @@ import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { osIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
import { SortDirection } from "../../../hooks/use-order-by";
|
||||
|
||||
function OperatingSystemsModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -16,7 +17,8 @@ function OperatingSystemsModal() {
|
||||
title: 'Operating Systems',
|
||||
dimension: 'os',
|
||||
endpoint: url.apiPath(site, '/operating-systems'),
|
||||
dimensionLabel: 'Operating system'
|
||||
dimensionLabel: 'Operating system',
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
|
@ -6,6 +6,7 @@ import { useQueryContext } from "../../../query-context";
|
||||
import { useSiteContext } from "../../../site-context";
|
||||
import { screenSizeIconFor } from "../../devices";
|
||||
import chooseMetrics from './choose-metrics';
|
||||
import { SortDirection } from "../../../hooks/use-order-by";
|
||||
|
||||
function ScreenSizesModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -15,7 +16,8 @@ function ScreenSizesModal() {
|
||||
title: 'Screen Sizes',
|
||||
dimension: 'screen',
|
||||
endpoint: url.apiPath(site, '/screen-sizes'),
|
||||
dimensionLabel: 'Screen size'
|
||||
dimensionLabel: 'Screen size',
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
|
@ -7,6 +7,7 @@ import * as metrics from '../reports/metrics'
|
||||
import * as url from '../../util/url';
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
import { SortDirection } from "../../hooks/use-order-by";
|
||||
|
||||
function EntryPagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -16,7 +17,8 @@ function EntryPagesModal() {
|
||||
title: 'Entry Pages',
|
||||
dimension: 'entry_page',
|
||||
endpoint: url.apiPath(site, '/entry-pages'),
|
||||
dimensionLabel: 'Entry page'
|
||||
dimensionLabel: 'Entry page',
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
@ -34,20 +36,20 @@ function EntryPagesModal() {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions', width: 'w-32' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors', width: 'w-36' })
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createVisits({ renderLabel: (_query) => "Total Entrances" }),
|
||||
metrics.createVisits({ renderLabel: (_query) => "Total Entrances", width: 'w-36' }),
|
||||
metrics.createVisitDuration()
|
||||
]
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import * as metrics from '../reports/metrics'
|
||||
import * as url from '../../util/url';
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
import { SortDirection } from "../../hooks/use-order-by";
|
||||
|
||||
function ExitPagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -16,7 +17,8 @@ function ExitPagesModal() {
|
||||
title: 'Exit Pages',
|
||||
dimension: 'exit_page',
|
||||
endpoint: url.apiPath(site, '/exit-pages'),
|
||||
dimensionLabel: 'Page url'
|
||||
dimensionLabel: 'Page url',
|
||||
defaultOrderBy: []
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
@ -34,14 +36,14 @@ function ExitPagesModal() {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions', width: 'w-32' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors', width: 'w-36' })
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,9 @@ function GoogleKeywordsModal() {
|
||||
|
||||
const metrics = [
|
||||
createVisitors({renderLabel: (_query) => 'Visitors'}),
|
||||
new Metric({key: 'impressions', renderLabel: (_query) => 'Impressions', renderValue: renderNumberWithTooltip}),
|
||||
new Metric({key: 'ctr', renderLabel: (_query) => 'CTR', renderValue: percentageFormatter}),
|
||||
new Metric({key: 'position', renderLabel: (_query) => 'Position', renderValue: numberFormatter})
|
||||
new Metric({width: 'w-28', key: 'impressions', renderLabel: (_query) => 'Impressions', renderValue: renderNumberWithTooltip, sortable: false}),
|
||||
new Metric({width: 'w-16', key: 'ctr', renderLabel: (_query) => 'CTR', renderValue: percentageFormatter, sortable: false}),
|
||||
new Metric({width: 'w-28', key: 'position', renderLabel: (_query) => 'Position', renderValue: numberFormatter, sortable: false})
|
||||
]
|
||||
|
||||
const {
|
||||
|
@ -8,11 +8,12 @@ import * as url from "../../util/url";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
import { addFilter } from "../../query";
|
||||
import { SortDirection } from "../../hooks/use-order-by";
|
||||
|
||||
const VIEWS = {
|
||||
countries: { title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country' },
|
||||
regions: { title: 'Top Regions', dimension: 'region', endpoint: '/regions', dimensionLabel: 'Region' },
|
||||
cities: { title: 'Top Cities', dimension: 'city', endpoint: '/cities', dimensionLabel: 'City' },
|
||||
countries: { title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country', defaultOrderBy: ["visitors", SortDirection.desc] },
|
||||
regions: { title: 'Top Regions', dimension: 'region', endpoint: '/regions', dimensionLabel: 'Region', defaultOrderBy: ["visitors", SortDirection.desc] },
|
||||
cities: { title: 'Top Cities', dimension: 'city', endpoint: '/cities', dimensionLabel: 'City', defaultOrderBy: ["visitors", SortDirection.desc] },
|
||||
}
|
||||
|
||||
function LocationsModal({ currentView }) {
|
||||
@ -38,14 +39,14 @@ function LocationsModal({ currentView }) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions', width: 'w-32' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors', width: 'w-36' })
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import * as metrics from '../reports/metrics'
|
||||
import * as url from '../../util/url';
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
import { SortDirection } from "../../hooks/use-order-by";
|
||||
|
||||
function PagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -16,7 +17,8 @@ function PagesModal() {
|
||||
title: 'Top Pages',
|
||||
dimension: 'page',
|
||||
endpoint: url.apiPath(site, '/pages'),
|
||||
dimensionLabel: 'Page url'
|
||||
dimensionLabel: 'Page url',
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
@ -34,14 +36,14 @@ function PagesModal() {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Conversions', width: 'w-32'}),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Current visitors', width: 'w-36'})
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import * as metrics from "../reports/metrics";
|
||||
import * as url from "../../util/url";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
import { SortDirection } from "../../hooks/use-order-by";
|
||||
|
||||
function PropsModal() {
|
||||
const { query } = useQueryContext();
|
||||
@ -23,7 +24,8 @@ function PropsModal() {
|
||||
title: specialTitleWhenGoalFilter(query, 'Custom Property Breakdown'),
|
||||
dimension: propKey,
|
||||
endpoint: url.apiPath(site, `/custom-prop-values/${propKey}`),
|
||||
dimensionLabel: propKey
|
||||
dimensionLabel: propKey,
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
|
@ -9,6 +9,7 @@ import * as url from "../../util/url";
|
||||
import { addFilter } from "../../query";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
import { SortDirection } from '../../hooks/use-order-by';
|
||||
|
||||
function ReferrerDrilldownModal() {
|
||||
const { referrer } = useParams();
|
||||
@ -19,7 +20,8 @@ function ReferrerDrilldownModal() {
|
||||
title: "Referrer Drilldown",
|
||||
dimension: 'referrer',
|
||||
endpoint: url.apiPath(site, `/referrers/${referrer}`),
|
||||
dimensionLabel: "Referrer"
|
||||
dimensionLabel: "Referrer",
|
||||
defaultOrderBy: ["visitors", SortDirection.desc]
|
||||
}
|
||||
|
||||
const getFilterInfo = useCallback((listItem) => {
|
||||
@ -37,14 +39,14 @@ function ReferrerDrilldownModal() {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions', width: 'w-32' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors', width: 'w-36' })
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import * as url from "../../util/url";
|
||||
import { addFilter } from "../../query";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
import { SortDirection } from "../../hooks/use-order-by";
|
||||
|
||||
const VIEWS = {
|
||||
sources: {
|
||||
@ -22,19 +23,19 @@ const VIEWS = {
|
||||
}
|
||||
},
|
||||
utm_mediums: {
|
||||
info: { title: 'Top UTM Mediums', dimension: 'utm_medium', endpoint: '/utm_mediums', dimensionLabel: 'UTM Medium' }
|
||||
info: { title: 'Top UTM Mediums', dimension: 'utm_medium', endpoint: '/utm_mediums', dimensionLabel: 'UTM Medium', defaultOrderBy: ["visitors", SortDirection.desc] }
|
||||
},
|
||||
utm_sources: {
|
||||
info: { title: 'Top UTM Sources', dimension: 'utm_source', endpoint: '/utm_sources', dimensionLabel: 'UTM Source' }
|
||||
info: { title: 'Top UTM Sources', dimension: 'utm_source', endpoint: '/utm_sources', dimensionLabel: 'UTM Source', defaultOrderBy: ["visitors", SortDirection.desc] }
|
||||
},
|
||||
utm_campaigns: {
|
||||
info: { title: 'Top UTM Campaigns', dimension: 'utm_campaign', endpoint: '/utm_campaigns', dimensionLabel: 'UTM Campaign' }
|
||||
info: { title: 'Top UTM Campaigns', dimension: 'utm_campaign', endpoint: '/utm_campaigns', dimensionLabel: 'UTM Campaign', defaultOrderBy: ["visitors", SortDirection.desc] }
|
||||
},
|
||||
utm_contents: {
|
||||
info: { title: 'Top UTM Contents', dimension: 'utm_content', endpoint: '/utm_contents', dimensionLabel: 'UTM Content' }
|
||||
info: { title: 'Top UTM Contents', dimension: 'utm_content', endpoint: '/utm_contents', dimensionLabel: 'UTM Content', defaultOrderBy: ["visitors", SortDirection.desc] }
|
||||
},
|
||||
utm_terms: {
|
||||
info: { title: 'Top UTM Terms', dimension: 'utm_term', endpoint: '/utm_terms', dimensionLabel: 'UTM Term' }
|
||||
info: { title: 'Top UTM Terms', dimension: 'utm_term', endpoint: '/utm_terms', dimensionLabel: 'UTM Term', defaultOrderBy: ["visitors", SortDirection.desc] }
|
||||
},
|
||||
}
|
||||
|
||||
@ -60,14 +61,14 @@ function SourcesModal({ currentView }) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions', width: 'w-32' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors', width: 'w-36' })
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ function EntryPages({ afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({ defaultLabel: 'Unique Entrances', meta: { plot: true } }),
|
||||
metrics.createVisitors({ defaultLabel: 'Unique Entrances', width: 'w-36', meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
].filter(metric => !!metric)
|
||||
}
|
||||
@ -70,7 +70,7 @@ function ExitPages({ afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({ defaultLabel: 'Unique Exits', meta: { plot: true } }),
|
||||
metrics.createVisitors({ defaultLabel: 'Unique Exits', width: 'w-36', meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
].filter(metric => !!metric)
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ export default function ListReport({ keyLabel, metrics, colMinWidth = COL_MIN_WI
|
||||
function renderReport() {
|
||||
if (state.list && state.list.length > 0) {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="h-full flex flex-col ">
|
||||
<div style={{ height: ROW_HEIGHT }}>
|
||||
{renderReportHeader()}
|
||||
</div>
|
||||
@ -191,7 +191,7 @@ export default function ListReport({ keyLabel, metrics, colMinWidth = COL_MIN_WI
|
||||
return (
|
||||
<div
|
||||
key={metric.key}
|
||||
className={`${metric.key} text-right ${hiddenOnMobileClass(metric)}`}
|
||||
className={`${metric.key} text-left ${hiddenOnMobileClass(metric)}`}
|
||||
style={{ minWidth: colMinWidth }}
|
||||
>
|
||||
{metric.renderLabel(query)}
|
||||
@ -200,7 +200,7 @@ export default function ListReport({ keyLabel, metrics, colMinWidth = COL_MIN_WI
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pt-3 w-full text-xs font-bold tracking-wide text-gray-500 flex items-center dark:text-gray-400">
|
||||
<div className="pt-3 w-full gap-x-4 text-xs font-bold tracking-wide text-gray-500 flex items-center dark:text-gray-400">
|
||||
<span className="flex-grow truncate">{keyLabel}</span>
|
||||
{metricLabels}
|
||||
</div>
|
||||
@ -218,7 +218,7 @@ export default function ListReport({ keyLabel, metrics, colMinWidth = COL_MIN_WI
|
||||
function renderRow(listItem) {
|
||||
return (
|
||||
<div key={listItem.name} style={{ minHeight: ROW_HEIGHT }}>
|
||||
<div className="flex w-full" style={{ marginTop: ROW_GAP_HEIGHT }}>
|
||||
<div className="flex w-full gap-x-4" style={{ marginTop: ROW_GAP_HEIGHT }}>
|
||||
{renderBarFor(listItem)}
|
||||
{renderMetricValuesFor(listItem)}
|
||||
</div>
|
||||
@ -268,10 +268,10 @@ export default function ListReport({ keyLabel, metrics, colMinWidth = COL_MIN_WI
|
||||
return (
|
||||
<div
|
||||
key={`${listItem.name}__${metric.key}`}
|
||||
className={`text-right ${hiddenOnMobileClass(metric)}`}
|
||||
className={`text-left ${hiddenOnMobileClass(metric)}`}
|
||||
style={{ width: colMinWidth, minWidth: colMinWidth }}
|
||||
>
|
||||
<span className="font-medium text-sm dark:text-gray-200 text-right">
|
||||
<span className="font-medium text-sm dark:text-gray-200 text-left">
|
||||
{metric.renderValue(listItem[metric.key])}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -52,7 +52,8 @@ export class Metric {
|
||||
this.renderValue = props.renderValue
|
||||
this.renderLabel = props.renderLabel
|
||||
this.meta = props.meta || {}
|
||||
this.sortable = props.sortable ?? true
|
||||
this.sortable = props.sortable
|
||||
this.width = props.width ?? 'w-24'
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,77 +89,77 @@ export const createVisitors = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
return new Metric({...props, key: "visitors", renderValue, renderLabel})
|
||||
return new Metric({width: 'w-24', sortable: true, ...props, key: "visitors", renderValue, renderLabel})
|
||||
}
|
||||
|
||||
export const createConversionRate = (props) => {
|
||||
const renderValue = percentageFormatter
|
||||
const renderLabel = (_query) => "CR"
|
||||
return new Metric({...props, key: "conversion_rate", renderLabel, renderValue, sortable: false})
|
||||
return new Metric({width: 'w-16', ...props, key: "conversion_rate", renderLabel, renderValue, sortable: false})
|
||||
}
|
||||
|
||||
export const createPercentage = (props) => {
|
||||
const renderValue = (value) => value
|
||||
const renderLabel = (_query) => "%"
|
||||
return new Metric({...props, key: "percentage", renderLabel, renderValue})
|
||||
return new Metric({width: 'w-16', ...props, key: "percentage", renderLabel, renderValue, sortable: true})
|
||||
}
|
||||
|
||||
export const createEvents = (props) => {
|
||||
const renderValue = typeof props.renderValue === 'function' ? props.renderValue : renderNumberWithTooltip
|
||||
return new Metric({...props, key: "events", renderValue: renderValue})
|
||||
return new Metric({width: 'w-24', ...props, key: "events", renderValue: renderValue, sortable: true})
|
||||
}
|
||||
|
||||
export const createTotalRevenue = (props) => {
|
||||
const renderValue = (value) => <Money formatted={value} />
|
||||
const renderLabel = (_query) => "Revenue"
|
||||
return new Metric({...props, key: "total_revenue", renderValue, renderLabel})
|
||||
return new Metric({width: 'w-16', ...props, key: "total_revenue", renderValue, renderLabel, sortable: true})
|
||||
}
|
||||
|
||||
export const createAverageRevenue = (props) => {
|
||||
const renderValue = (value) => <Money formatted={value} />
|
||||
const renderLabel = (_query) => "Average"
|
||||
return new Metric({...props, key: "average_revenue", renderValue, renderLabel})
|
||||
return new Metric({width: 'w-24', ...props, key: "average_revenue", renderValue, renderLabel, sortable: true})
|
||||
}
|
||||
|
||||
export const createTotalVisitors = (props) => {
|
||||
const renderValue = renderNumberWithTooltip
|
||||
const renderLabel = (_query) => "Total Visitors"
|
||||
return new Metric({...props, key: "total_visitors", renderValue, renderLabel, sortable: false })
|
||||
return new Metric({width: 'w-32', ...props, key: "total_visitors", renderValue, renderLabel, sortable: false})
|
||||
}
|
||||
|
||||
export const createVisits = (props) => {
|
||||
const renderValue = renderNumberWithTooltip
|
||||
return new Metric({...props, key: "visits", renderValue})
|
||||
return new Metric({width: 'w-24', sortable: true, ...props, key: "visits", renderValue })
|
||||
}
|
||||
|
||||
export const createVisitDuration = (props) => {
|
||||
const renderValue = durationFormatter
|
||||
const renderLabel = (_query) => "Visit Duration"
|
||||
return new Metric({...props, key: "visit_duration", renderValue, renderLabel})
|
||||
return new Metric({width: 'w-36', ...props, key: "visit_duration", renderValue, renderLabel, sortable: true})
|
||||
}
|
||||
|
||||
export const createBounceRate = (props) => {
|
||||
const renderValue = (value) => `${value}%`
|
||||
const renderLabel = (_query) => "Bounce Rate"
|
||||
return new Metric({...props, key: "bounce_rate", renderValue, renderLabel})
|
||||
return new Metric({width: 'w-36', ...props, key: "bounce_rate", renderValue, renderLabel, sortable: true})
|
||||
}
|
||||
|
||||
export const createPageviews = (props) => {
|
||||
const renderValue = renderNumberWithTooltip
|
||||
const renderLabel = (_query) => "Pageviews"
|
||||
return new Metric({...props, key: "pageviews", renderValue, renderLabel})
|
||||
return new Metric({width: 'w-28', ...props, key: "pageviews", renderValue, renderLabel, sortable: true})
|
||||
}
|
||||
|
||||
export const createTimeOnPage = (props) => {
|
||||
const renderValue = durationFormatter
|
||||
const renderLabel = (_query) => "Time on Page"
|
||||
return new Metric({...props, key: "time_on_page", renderValue, renderLabel, sortable: false})
|
||||
return new Metric({width: 'w-32', ...props, key: "time_on_page", renderValue, renderLabel, sortable: false})
|
||||
}
|
||||
|
||||
export const createExitRate = (props) => {
|
||||
const renderValue = percentageFormatter
|
||||
const renderLabel = (_query) => "Exit Rate"
|
||||
return new Metric({...props, key: "exit_rate", renderValue, renderLabel, sortable: false})
|
||||
return new Metric({width: 'w-28', ...props, key: "exit_rate", renderValue, renderLabel, sortable: false})
|
||||
}
|
||||
|
||||
export function renderNumberWithTooltip(value) {
|
||||
|
Loading…
Reference in New Issue
Block a user