Skip to content

Commit

Permalink
Allow sorting breakdown lists by some metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
apata committed Sep 3, 2024
1 parent 533bf90 commit 7865bbc
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 42 deletions.
23 changes: 23 additions & 0 deletions assets/js/dashboard/components/sort-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** @format */

import React, { ReactNode } from 'react'
import { getSortDirectionIndicator, SortDirection } from '../hooks/use-order-by'

export const SortButton = ({
children,
toggleSort,
hint,
sortDirection
}: {
children: ReactNode
toggleSort: () => void
hint: string
sortDirection: SortDirection | null
}) => {
return (
<button onClick={toggleSort} title={hint} className='hover:underline'>
{children}
{sortDirection !== null && <span> {getSortDirectionIndicator(sortDirection)}</span>}
</button>
)
}
5 changes: 3 additions & 2 deletions assets/js/dashboard/hooks/api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect } from "react"
import { useQueryClient, useInfiniteQuery } from "@tanstack/react-query"
import * as api from "../api"

const LIMIT = 100
const LIMIT = 10 // FOR DEBUGGING

/**
* A wrapper for the React Query library. Constructs the necessary options
Expand Down Expand Up @@ -78,6 +78,7 @@ export function useAPIClient(props) {
queryKey: key,
queryFn,
getNextPageParam,
initialPageParam
initialPageParam,
placeholderData: (previousData) => previousData,
})
}
113 changes: 113 additions & 0 deletions assets/js/dashboard/hooks/use-order-by.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/** @format */

import { Metric } from '../stats/reports/metrics'
import {
OrderBy,
SortDirection,
cycleSortDirection,
findOrderIndex,
omitOrderByIndex,
rearrangeOrderBy
} from './use-order-by'

describe(`${findOrderIndex.name}`, () => {
/* prettier-ignore */
const cases: [OrderBy, Pick<Metric, 'key'>, number][] = [
[[], { key: 'anything' }, -1],
[[['visitors', SortDirection.asc]], { key: 'anything' }, -1],
[[['bounce_rate', SortDirection.desc], ['visitors', SortDirection.asc]], {key: 'visitors'}, 1]
]

test.each(cases)(
`in order by %p, the index of metric %p is %p`,
(orderBy, metric, expectedIndex) => {
expect(findOrderIndex(orderBy, metric)).toEqual(expectedIndex)
}
)
})

describe(`${omitOrderByIndex.name}`, () => {
/* prettier-ignore */
const cases: [OrderBy, number, OrderBy][] = [
[[['visitors', SortDirection.asc]], 0, []],
[[['bounce_rate', SortDirection.desc], ['visitors', SortDirection.asc]], 1, [['bounce_rate', SortDirection.desc]]],
[[['a', SortDirection.desc], ['b', SortDirection.asc], ['c', SortDirection.desc]], 1, [['a', SortDirection.desc], ['c', SortDirection.desc]]]
]

test.each(cases)(
`in order by %p, omitting the index %p yields %p`,
(orderBy, index, expectedOrderBy) => {
expect(omitOrderByIndex(orderBy, index)).toEqual(expectedOrderBy)
}
)
})

describe(`${cycleSortDirection.name}`, () => {
test.each([
[
null,
{
direction: SortDirection.desc,
hint: 'Press to sort column in descending order'
}
],
[
SortDirection.desc,
{
direction: SortDirection.asc,
hint: 'Press to sort column in ascending order'
}
],
[
SortDirection.asc,
{
direction: null,
hint: 'Press to remove sorting from column'
}
]
])(
'for current direction %p returns %p',
(currentDirection, expectedOutput) => {
expect(cycleSortDirection(currentDirection)).toEqual(expectedOutput)
}
)
})

describe(`${rearrangeOrderBy.name}`, () => {
const cases: [Pick<Metric, 'key'>, OrderBy, OrderBy][] = [
[{ key: 'visitors' }, [['visitors', SortDirection.asc]], []],
[
{ key: 'bounce_rate' },
[
['bounce_rate', SortDirection.desc],
['visitors', SortDirection.asc]
],
[
['bounce_rate', SortDirection.asc],
['visitors', SortDirection.asc]
]
],
[
{ key: 'visitors' },
[
['bounce_rate', SortDirection.desc],
['visitors', SortDirection.asc]
],
[['bounce_rate', SortDirection.desc]]
],
[
{ key: 'visit_duration' },
[['bounce_rate', SortDirection.desc]],
[
['visit_duration', SortDirection.desc],
['bounce_rate', SortDirection.desc]
]
]
]
it.each(cases)(
'clicking on %p with order %p yields %p',
(metric, currentOrderBy, expectedOrderBy) => {
expect(rearrangeOrderBy(currentOrderBy, metric)).toEqual(expectedOrderBy)
}
)
})
83 changes: 83 additions & 0 deletions assets/js/dashboard/hooks/use-order-by.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/** @format */

import { useCallback, useMemo, useState } from 'react'
import { Metric } from '../stats/reports/metrics'

export enum SortDirection {
asc = 'asc',
desc = 'desc'
}

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[] }) {
const [orderBy, setOrderBy] = useState<OrderBy>([])
const orderByDictionary = useMemo(
() => Object.fromEntries(orderBy),
[orderBy]
)

const toggleSortByMetric = useCallback(
(metric: Pick<Metric, 'key'>) => {
if (!metrics.find(({ key }) => key === metric.key)) {
return;
}
setOrderBy((currentOrderBy) => rearrangeOrderBy(currentOrderBy, metric))
},
[metrics]
)

return { orderBy, orderByDictionary, toggleSortByMetric }
}

export function cycleSortDirection(
currentSortDirection: SortDirection | null
): {direction: SortDirection | null, hint: string} {
switch (currentSortDirection) {
case null:
return {direction: SortDirection.desc, hint: "Press to sort column in descending order"}
case SortDirection.desc:
return {direction: SortDirection.asc, hint: "Press to sort column in ascending order"}
case SortDirection.asc:
return {direction: null, hint: "Press to remove sorting from column"}
}
}

export function findOrderIndex(orderBy: OrderBy, metric: Pick<Metric, 'key'>) {
return orderBy.findIndex(([metricKey]) => metricKey === metric.key)
}

export function omitOrderByIndex(orderBy: OrderBy, index: number) {
return orderBy.slice(0, index).concat(orderBy.slice(index + 1))
}

export function rearrangeOrderBy(currentOrderBy: OrderBy, metric: Pick<Metric, 'key'>): OrderBy {
const orderIndex = findOrderIndex(currentOrderBy, metric)
if (orderIndex < 0) {
const sortDirection = cycleSortDirection(null).direction as SortDirection
return [[metric.key, sortDirection], ...currentOrderBy]
}
const previousOrder = currentOrderBy[orderIndex]
const sortDirection = cycleSortDirection(previousOrder[1]).direction
if (sortDirection === null) {
return omitOrderByIndex(currentOrderBy, orderIndex)
}
return [
[metric.key, sortDirection],
...omitOrderByIndex(currentOrderBy, orderIndex)
]

}
3 changes: 1 addition & 2 deletions assets/js/dashboard/stats/locations/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ const WorldMap = ({
const navigate = useAppNavigate()
const { mode } = useTheme()
const site = useSiteContext()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { query } = useQueryContext() as { query: any }
const { query } = useQueryContext()
const svgRef = useRef<SVGSVGElement | null>(null)
const [tooltip, setTooltip] = useState<{
x: number
Expand Down
Loading

0 comments on commit 7865bbc

Please sign in to comment.