mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 01:22:15 +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 { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import classNames from 'classnames'
|
||||
import * as storage from './util/storage'
|
||||
|
||||
const COMPARISON_MODES = {
|
||||
'off': 'Disable comparison',
|
||||
'previous_period': 'Previous period',
|
||||
'year_over_year': 'Year over year',
|
||||
}
|
||||
|
||||
const DEFAULT_COMPARISON_MODE = 'previous_period'
|
||||
|
||||
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 }) {
|
||||
if (!site.flags.comparisons) 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 })
|
||||
}
|
||||
|
||||
function renderItem({ label, value, isCurrentlySelected }) {
|
||||
const labelClass = classNames("font-medium text-sm", { "font-bold disabled": isCurrentlySelected })
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
key={value}
|
||||
onClick={() => update(value)}
|
||||
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">
|
||||
<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">
|
||||
<>
|
||||
<span className="pl-2 text-sm font-medium text-gray-800 dark:text-gray-200">vs.</span>
|
||||
<div className="flex">
|
||||
<div className="min-w-32 md:w-52 md:relative">
|
||||
<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">
|
||||
<span>{ 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" />
|
||||
<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 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-2" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
@ -50,13 +93,13 @@ const ComparisonInput = function({ site, query, history }) {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
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>
|
||||
{ renderItem({ label: "Disabled", value: false, isCurrentlySelected: !query.comparison }) }
|
||||
{ Object.keys(COMPARISON_MODES).map((key) => renderItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison})) }
|
||||
{ Object.keys(COMPARISON_MODES).map((key) => DropdownItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode })) }
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
} from "./util/date";
|
||||
import { navigateToQuery, QueryLink, QueryButton } from "./query";
|
||||
import { shouldIgnoreKeypress } from "./keybinding.js"
|
||||
import { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input.js"
|
||||
|
||||
function renderArrow(query, site, period, prevDate, nextDate) {
|
||||
const insertionDate = parseUTCDate(site.statsBegin);
|
||||
@ -194,11 +195,24 @@ function DatePicker({query, site, history}) {
|
||||
|
||||
setOpen(false);
|
||||
|
||||
const keys = ['d', 'e', 'r', 'w', 'm', 'y', 't', 's', 'l', 'a'];
|
||||
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'}];
|
||||
const keybindings = {
|
||||
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())) {
|
||||
navigateToQuery(history, query, {...newSearch, ...(redirects[keys.indexOf(e.key.toLowerCase())])});
|
||||
const redirect = keybindings[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') {
|
||||
setOpen(true)
|
||||
setMode('calendar')
|
||||
@ -352,6 +366,18 @@ function DatePicker({query, site, history}) {
|
||||
<span className='font-normal'>C</span>
|
||||
</span>
|
||||
</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>
|
||||
);
|
||||
@ -381,7 +407,7 @@ function DatePicker({query, site, history}) {
|
||||
function renderPicker() {
|
||||
return (
|
||||
<div
|
||||
className="w-20 sm:w-36 md:w-48 md:relative"
|
||||
className="min-w-32 md:w-48 md:relative"
|
||||
ref={dropDownNode}
|
||||
>
|
||||
<div
|
||||
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { Link, withRouter } from 'react-router-dom'
|
||||
import {formatDay, formatMonthYYYY, nowForSite, parseUTCDate} from './util/date'
|
||||
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']
|
||||
|
||||
@ -19,16 +19,16 @@ export function parseQuery(querystring, site) {
|
||||
period = '30d'
|
||||
}
|
||||
|
||||
let comparison = q.get('comparison')
|
||||
if (COMPARISON_DISABLED_PERIODS.includes(period)) comparison = null
|
||||
let comparison = q.get('comparison') || getStoredComparisonMode(site.domain)
|
||||
if (COMPARISON_DISABLED_PERIODS.includes(period) || !isComparisonEnabled(comparison)) comparison = null
|
||||
|
||||
return {
|
||||
period,
|
||||
comparison,
|
||||
date: q.get('date') ? parseUTCDate(q.get('date')) : nowForSite(site),
|
||||
from: q.get('from') ? parseUTCDate(q.get('from')) : undefined,
|
||||
to: q.get('to') ? parseUTCDate(q.get('to')) : undefined,
|
||||
with_imported: q.get('with_imported') ? q.get('with_imported') === 'true' : true,
|
||||
comparison: comparison,
|
||||
filters: {
|
||||
'goal': q.get('goal'),
|
||||
'props': JSON.parse(q.get('props')),
|
||||
|
Loading…
Reference in New Issue
Block a user