mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 09:33:19 +03:00
Improve comparisons navigation (#2736)
This pull request enhances the navigation for comparisons by implementing the following changes: * Hides the comparison input behind the period picker, making the top bar less busy. * Adds a key binding for the comparison input (assigned to x) alongside arrow navigation. * Persists the selected comparison mode in Local Storage by domain, resulting in its persistence between refreshes.
This commit is contained in:
parent
b17710a706
commit
49e4c2fb64
@ -4,42 +4,85 @@ import { navigateToQuery } from './query'
|
|||||||
import { Menu, Transition } from '@headlessui/react'
|
import { Menu, Transition } from '@headlessui/react'
|
||||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import * as storage from './util/storage'
|
||||||
|
|
||||||
const COMPARISON_MODES = {
|
const COMPARISON_MODES = {
|
||||||
|
'off': 'Disable comparison',
|
||||||
'previous_period': 'Previous period',
|
'previous_period': 'Previous period',
|
||||||
'year_over_year': 'Year over year',
|
'year_over_year': 'Year over year',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_COMPARISON_MODE = 'previous_period'
|
||||||
|
|
||||||
export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all']
|
export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all']
|
||||||
|
|
||||||
|
export const getStoredComparisonMode = function(domain) {
|
||||||
|
const mode = storage.getItem(`comparison_mode__${domain}`)
|
||||||
|
if (Object.keys(COMPARISON_MODES).includes(mode)) {
|
||||||
|
return mode
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeComparisonMode = function(domain, mode) {
|
||||||
|
storage.setItem(`comparison_mode__${domain}`, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isComparisonEnabled = function(mode) {
|
||||||
|
return mode && mode !== "off"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleComparisons = function(history, query, site) {
|
||||||
|
if (!site.flags.comparisons) return
|
||||||
|
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return
|
||||||
|
|
||||||
|
if (isComparisonEnabled(query.comparison)) {
|
||||||
|
storeComparisonMode(site.domain, "off")
|
||||||
|
navigateToQuery(history, query, { comparison: "off" })
|
||||||
|
} else {
|
||||||
|
const storedMode = getStoredComparisonMode(site.domain)
|
||||||
|
const newMode = isComparisonEnabled(storedMode) ? storedMode : DEFAULT_COMPARISON_MODE
|
||||||
|
|
||||||
|
storeComparisonMode(site.domain, newMode)
|
||||||
|
navigateToQuery(history, query, { comparison: newMode })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownItem({ label, value, isCurrentlySelected, updateMode }) {
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={value}
|
||||||
|
onClick={() => updateMode(value)}
|
||||||
|
disabled={isCurrentlySelected}>
|
||||||
|
{({ active }) => (
|
||||||
|
<button className={classNames("px-4 py-2 w-full text-left font-medium text-sm dark:text-white cursor-pointer", { "bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100": active, "font-bold": isCurrentlySelected })}>
|
||||||
|
{ label }
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ComparisonInput = function({ site, query, history }) {
|
const ComparisonInput = function({ site, query, history }) {
|
||||||
if (!site.flags.comparisons) return null
|
if (!site.flags.comparisons) return null
|
||||||
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
|
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
|
||||||
|
if (!isComparisonEnabled(query.comparison)) return null
|
||||||
|
|
||||||
function update(key) {
|
const updateMode = (key) => {
|
||||||
|
storeComparisonMode(site.domain, key)
|
||||||
navigateToQuery(history, query, { comparison: key })
|
navigateToQuery(history, query, { comparison: key })
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItem({ label, value, isCurrentlySelected }) {
|
|
||||||
const labelClass = classNames("font-medium text-sm", { "font-bold disabled": isCurrentlySelected })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.Item
|
<>
|
||||||
key={value}
|
<span className="pl-2 text-sm font-medium text-gray-800 dark:text-gray-200">vs.</span>
|
||||||
onClick={() => update(value)}
|
<div className="flex">
|
||||||
className="px-4 py-2 leading-tight hover:bg-gray-100 dark:text-white hover:text-gray-900 dark:hover:bg-gray-900 dark:hover:text-gray-100 flex hover:cursor-pointer">
|
<div className="min-w-32 md:w-52 md:relative">
|
||||||
<span className={labelClass}>{ label }</span>
|
|
||||||
</Menu.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex ml-auto pl-2">
|
|
||||||
<div className="w-20 sm:w-36 md:w-48 md:relative">
|
|
||||||
<Menu as="div" className="relative inline-block pl-2 w-full">
|
<Menu as="div" className="relative inline-block pl-2 w-full">
|
||||||
<Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow truncate cursor-pointer w-full">
|
<Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow cursor-pointer w-full truncate">
|
||||||
<span>{ COMPARISON_MODES[query.comparison] || 'Compare to' }</span>
|
<span className="truncate">{ COMPARISON_MODES[query.comparison] || 'Compare to' }</span>
|
||||||
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-5" />
|
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-2" aria-hidden="true" />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
@ -50,13 +93,13 @@ const ComparisonInput = function({ site, query, history }) {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95">
|
leaveTo="transform opacity-0 scale-95">
|
||||||
<Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10" static>
|
<Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10" static>
|
||||||
{ renderItem({ label: "Disabled", value: false, isCurrentlySelected: !query.comparison }) }
|
{ Object.keys(COMPARISON_MODES).map((key) => DropdownItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode })) }
|
||||||
{ Object.keys(COMPARISON_MODES).map((key) => renderItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison})) }
|
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
} from "./util/date";
|
} from "./util/date";
|
||||||
import { navigateToQuery, QueryLink, QueryButton } from "./query";
|
import { navigateToQuery, QueryLink, QueryButton } from "./query";
|
||||||
import { shouldIgnoreKeypress } from "./keybinding.js"
|
import { shouldIgnoreKeypress } from "./keybinding.js"
|
||||||
|
import { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input.js"
|
||||||
|
|
||||||
function renderArrow(query, site, period, prevDate, nextDate) {
|
function renderArrow(query, site, period, prevDate, nextDate) {
|
||||||
const insertionDate = parseUTCDate(site.statsBegin);
|
const insertionDate = parseUTCDate(site.statsBegin);
|
||||||
@ -194,11 +195,24 @@ function DatePicker({query, site, history}) {
|
|||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
const keys = ['d', 'e', 'r', 'w', 'm', 'y', 't', 's', 'l', 'a'];
|
const keybindings = {
|
||||||
const redirects = [{date: false, period: 'day'}, {date: formatISO(shiftDays(nowForSite(site), -1)), period: 'day'}, {period: 'realtime'}, {date: false, period: '7d'}, {date: false, period: 'month'}, {date: false, period: 'year'}, {date: false, period: '30d'}, {date: false, period: '6mo'}, {date: false, period: '12mo'}, {date: false, period: 'all'}];
|
d: {date: false, period: 'day'},
|
||||||
|
e: {date: formatISO(shiftDays(nowForSite(site), -1)), period: 'day'},
|
||||||
|
r: {period: 'realtime'},
|
||||||
|
w: {date: false, period: '7d'},
|
||||||
|
m: {date: false, period: 'month'},
|
||||||
|
y: {date: false, period: 'year'},
|
||||||
|
t: {date: false, period: '30d'},
|
||||||
|
s: {date: false, period: '6mo'},
|
||||||
|
l: {date: false, period: '12mo'},
|
||||||
|
a: {date: false, period: 'all'},
|
||||||
|
}
|
||||||
|
|
||||||
if (keys.includes(e.key.toLowerCase())) {
|
const redirect = keybindings[e.key.toLowerCase()]
|
||||||
navigateToQuery(history, query, {...newSearch, ...(redirects[keys.indexOf(e.key.toLowerCase())])});
|
if (redirect) {
|
||||||
|
navigateToQuery(history, query, {...newSearch, ...redirect})
|
||||||
|
} else if (e.key.toLowerCase() === 'x') {
|
||||||
|
toggleComparisons(history, query, site)
|
||||||
} else if (e.key.toLowerCase() === 'c') {
|
} else if (e.key.toLowerCase() === 'c') {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setMode('calendar')
|
setMode('calendar')
|
||||||
@ -352,6 +366,18 @@ function DatePicker({query, site, history}) {
|
|||||||
<span className='font-normal'>C</span>
|
<span className='font-normal'>C</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{ !COMPARISON_DISABLED_PERIODS.includes(query.period) && site.flags.comparisons &&
|
||||||
|
<div className="py-1 date-option-group border-t border-gray-200 dark:border-gray-500">
|
||||||
|
<span
|
||||||
|
onClick={() => {
|
||||||
|
toggleComparisons(history, query, site)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 text-sm leading-tight hover:bg-gray-100 dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer flex items-center justify-between">
|
||||||
|
{ isComparisonEnabled(query.comparison) ? 'Disable comparison' : 'Compare' }
|
||||||
|
<span className='font-normal'>X</span>
|
||||||
|
</span>
|
||||||
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -381,7 +407,7 @@ function DatePicker({query, site, history}) {
|
|||||||
function renderPicker() {
|
function renderPicker() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-20 sm:w-36 md:w-48 md:relative"
|
className="min-w-32 md:w-48 md:relative"
|
||||||
ref={dropDownNode}
|
ref={dropDownNode}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import { Link, withRouter } from 'react-router-dom'
|
import { Link, withRouter } from 'react-router-dom'
|
||||||
import {formatDay, formatMonthYYYY, nowForSite, parseUTCDate} from './util/date'
|
import {formatDay, formatMonthYYYY, nowForSite, parseUTCDate} from './util/date'
|
||||||
import * as storage from './util/storage'
|
import * as storage from './util/storage'
|
||||||
import { COMPARISON_DISABLED_PERIODS } from './comparison-input'
|
import { COMPARISON_DISABLED_PERIODS, getStoredComparisonMode, isComparisonEnabled } from './comparison-input'
|
||||||
|
|
||||||
const PERIODS = ['realtime', 'day', 'month', '7d', '30d', '6mo', '12mo', 'year', 'all', 'custom']
|
const PERIODS = ['realtime', 'day', 'month', '7d', '30d', '6mo', '12mo', 'year', 'all', 'custom']
|
||||||
|
|
||||||
@ -19,16 +19,16 @@ export function parseQuery(querystring, site) {
|
|||||||
period = '30d'
|
period = '30d'
|
||||||
}
|
}
|
||||||
|
|
||||||
let comparison = q.get('comparison')
|
let comparison = q.get('comparison') || getStoredComparisonMode(site.domain)
|
||||||
if (COMPARISON_DISABLED_PERIODS.includes(period)) comparison = null
|
if (COMPARISON_DISABLED_PERIODS.includes(period) || !isComparisonEnabled(comparison)) comparison = null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
period,
|
period,
|
||||||
|
comparison,
|
||||||
date: q.get('date') ? parseUTCDate(q.get('date')) : nowForSite(site),
|
date: q.get('date') ? parseUTCDate(q.get('date')) : nowForSite(site),
|
||||||
from: q.get('from') ? parseUTCDate(q.get('from')) : undefined,
|
from: q.get('from') ? parseUTCDate(q.get('from')) : undefined,
|
||||||
to: q.get('to') ? parseUTCDate(q.get('to')) : undefined,
|
to: q.get('to') ? parseUTCDate(q.get('to')) : undefined,
|
||||||
with_imported: q.get('with_imported') ? q.get('with_imported') === 'true' : true,
|
with_imported: q.get('with_imported') ? q.get('with_imported') === 'true' : true,
|
||||||
comparison: comparison,
|
|
||||||
filters: {
|
filters: {
|
||||||
'goal': q.get('goal'),
|
'goal': q.get('goal'),
|
||||||
'props': JSON.parse(q.get('props')),
|
'props': JSON.parse(q.get('props')),
|
||||||
|
Loading…
Reference in New Issue
Block a user