Merge pull request #72 from mikecao/dev

v0.17.0
This commit is contained in:
Mike Cao 2020-08-28 00:08:13 -07:00 committed by GitHub
commit 886bd7679e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 742 additions and 355 deletions

View File

@ -16,14 +16,17 @@ import BrowsersTable from './metrics/BrowsersTable';
import OSTable from './metrics/OSTable'; import OSTable from './metrics/OSTable';
import DevicesTable from './metrics/DevicesTable'; import DevicesTable from './metrics/DevicesTable';
import CountriesTable from './metrics/CountriesTable'; import CountriesTable from './metrics/CountriesTable';
import EventsTable from './metrics/EventsTable';
import EventsChart from './metrics/EventsChart';
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) { export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
const [data, setData] = useState(); const [data, setData] = useState();
const [chartLoaded, setChartLoaded] = useState(false); const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState(); const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange)); const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const [expand, setExpand] = useState(); const [expand, setExpand] = useState();
const { startDate, endDate } = dateRange; const { startDate, endDate, unit } = dateRange;
const BackButton = () => ( const BackButton = () => (
<Button <Button
@ -50,12 +53,18 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
value: 'country', value: 'country',
component: props => <CountriesTable {...props} onDataLoad={data => setCountryData(data)} />, component: props => <CountriesTable {...props} onDataLoad={data => setCountryData(data)} />,
}, },
{ label: 'Events', value: 'event', component: EventsTable },
]; ];
const tableProps = { const dataProps = {
websiteId, websiteId,
startDate, startDate,
endDate, endDate,
unit,
};
const tableProps = {
...dataProps,
limit: 10, limit: 10,
onExpand: handleExpand, onExpand: handleExpand,
websiteDomain: data?.domain, websiteDomain: data?.domain,
@ -63,6 +72,10 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
const DetailsComponent = expand?.component; const DetailsComponent = expand?.component;
function getSelectedMenuOption(value) {
return menuOptions.find(e => e.value === value);
}
async function loadData() { async function loadData() {
setData(await get(`/api/website/${websiteId}`)); setData(await get(`/api/website/${websiteId}`));
} }
@ -76,11 +89,11 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
} }
function handleExpand(value) { function handleExpand(value) {
setExpand(menuOptions.find(e => e.value === value)); setExpand(getSelectedMenuOption(value));
} }
function handleMenuSelect(value) { function handleMenuSelect(value) {
setExpand(menuOptions.find(e => e.value === value)); setExpand(getSelectedMenuOption(value));
} }
useEffect(() => { useEffect(() => {
@ -132,7 +145,17 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
<WorldMap data={countryData} /> <WorldMap data={countryData} />
</div> </div>
<div className="col-12 col-md-12 col-lg-4"> <div className="col-12 col-md-12 col-lg-4">
<CountriesTable {...tableProps} onDataLoad={data => setCountryData(data)} /> <CountriesTable {...tableProps} onDataLoad={setCountryData} />
</div>
</div>
<div
className={classNames(styles.row, 'row', { [styles.hidden]: !eventsData?.length > 0 })}
>
<div className="col-12 col-md-12 col-lg-4">
<EventsTable {...tableProps} onDataLoad={setEventsData} />
</div>
<div className="col-12 col-md-12 col-lg-8 pt-5 pb-5">
<EventsChart {...dataProps} />
</div> </div>
</div> </div>
</> </>

View File

@ -42,6 +42,10 @@
padding-right: 0; padding-right: 0;
} }
.hidden {
display: none;
}
@media only screen and (max-width: 992px) { @media only screen and (max-width: 992px) {
.row { .row {
border: 0; border: 0;

View File

@ -2,13 +2,13 @@
display: inline-flex; display: inline-flex;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
border: 1px solid var(--gray400); border: 1px solid var(--gray500);
} }
.group .button { .group .button {
border-radius: 0; border-radius: 0;
background: var(--gray50); background: var(--gray50);
border-left: 1px solid var(--gray400); border-left: 1px solid var(--gray500);
padding: 4px 8px; padding: 4px 8px;
} }
@ -16,6 +16,10 @@
border: 0; border: 0;
} }
.group .button:hover {
background: var(--gray100);
}
.group .button + .button { .group .button + .button {
margin: 0; margin: 0;
} }

View File

@ -0,0 +1,13 @@
import React from 'react';
import classNames from 'classnames';
import styles from './Loading.module.css';
export default function Loading({ className }) {
return (
<div className={classNames(styles.loading, className)}>
<div />
<div />
<div />
</div>
);
}

View File

@ -0,0 +1,41 @@
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
.loading {
display: flex;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
}
.loading div {
width: 10px;
height: 10px;
border-radius: 100%;
background: var(--gray400);
animation: blink 1.4s infinite;
animation-fill-mode: both;
}
.loading div + div {
margin-left: 10px;
}
.loading div:nth-child(2) {
animation-delay: 0.2s;
}
.loading div:nth-child(3) {
animation-delay: 0.4s;
}

View File

@ -10,6 +10,7 @@
} }
.container .content { .container .content {
position: relative;
border-left: 1px solid var(--gray300); border-left: 1px solid var(--gray300);
padding-left: 30px; padding-left: 30px;
} }

View File

@ -0,0 +1,168 @@
import React, { useState, useRef, useEffect } from 'react';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import ChartJS from 'chart.js';
import styles from './BarChart.module.css';
import { format } from 'date-fns';
export default function BarChart({
chartId,
datasets,
unit,
records,
height = 400,
animationDuration = 300,
className,
stacked = false,
onCreate = () => {},
onUpdate = () => {},
}) {
const canvas = useRef();
const chart = useRef();
const [tooltip, setTooltip] = useState({});
const renderLabel = (label, index, values) => {
const d = new Date(values[index].value);
const n = records;
switch (unit) {
case 'hour':
return format(d, 'ha');
case 'day':
if (n >= 15) {
return index % ~~(n / 15) === 0 ? format(d, 'MMM d') : '';
}
return format(d, 'EEE M/d');
case 'month':
return format(d, 'MMMM');
default:
return label;
}
};
const renderTooltip = model => {
const { opacity, title, body, labelColors } = model;
if (!opacity) {
setTooltip(null);
} else {
const [label, value] = body[0].lines[0].split(':');
setTooltip({
title: title[0],
value,
label,
labelColor: labelColors[0].backgroundColor,
});
}
};
const createChart = () => {
const options = {
animation: {
duration: animationDuration,
},
tooltips: {
enabled: false,
custom: renderTooltip,
},
hover: {
animationDuration: 0,
},
responsive: true,
responsiveAnimationDuration: 0,
maintainAspectRatio: false,
scales: {
xAxes: [
{
type: 'time',
distribution: 'series',
time: {
unit,
tooltipFormat: 'ddd MMMM DD YYYY',
},
ticks: {
callback: renderLabel,
minRotation: 0,
maxRotation: 0,
},
gridLines: {
display: false,
},
offset: true,
stacked: true,
},
],
yAxes: [
{
ticks: {
beginAtZero: true,
},
stacked,
},
],
},
};
onCreate(options);
chart.current = new ChartJS(canvas.current, {
type: 'bar',
data: {
datasets,
},
options,
});
};
const updateChart = () => {
const { options } = chart.current;
options.scales.xAxes[0].time.unit = unit;
options.scales.xAxes[0].ticks.callback = renderLabel;
options.animation.duration = animationDuration;
onUpdate(chart.current);
};
useEffect(() => {
if (datasets) {
if (!chart.current) {
createChart();
} else {
setTooltip(null);
updateChart();
}
}
}, [datasets, unit, animationDuration]);
return (
<>
<div
data-tip=""
data-for={`${chartId}-tooltip`}
className={classNames(styles.chart, className)}
style={{ height }}
>
<canvas ref={canvas} />
</div>
<ReactTooltip id={`${chartId}-tooltip`}>
{tooltip ? <Tooltip {...tooltip} /> : null}
</ReactTooltip>
</>
);
}
const Tooltip = ({ title, value, label, labelColor }) => (
<div className={styles.tooltip}>
<div className={styles.content}>
<div className={styles.title}>{title}</div>
<div className={styles.metric}>
<div className={styles.dot}>
<div className={styles.color} style={{ backgroundColor: labelColor }} />
</div>
{value} {label}
</div>
</div>
</div>
);

View File

@ -0,0 +1,96 @@
import React, { useState, useEffect, useMemo } from 'react';
import classNames from 'classnames';
import tinycolor from 'tinycolor2';
import BarChart from './BarChart';
import { get } from 'lib/web';
import { getTimezone, getDateArray, getDateLength } from 'lib/date';
import styles from './BarChart.module.css';
const COLORS = [
'#2680eb',
'#9256d9',
'#44b556',
'#e68619',
'#e34850',
'#1b959a',
'#d83790',
'#85d044',
];
export default function EventsChart({ websiteId, startDate, endDate, unit }) {
const [data, setData] = useState();
const datasets = useMemo(() => {
if (!data) return [];
return Object.keys(data).map((key, index) => {
const color = tinycolor(COLORS[index]);
return {
label: key,
data: data[key],
lineTension: 0,
backgroundColor: color.setAlpha(0.4).toRgbString(),
borderColor: color.setAlpha(0.5).toRgbString(),
borderWidth: 1,
};
});
}, [data]);
async function loadData() {
const data = await get(`/api/website/${websiteId}/events`, {
start_at: +startDate,
end_at: +endDate,
unit,
tz: getTimezone(),
});
const map = data.reduce((obj, { x, t, y }) => {
if (!obj[x]) {
obj[x] = [];
}
obj[x].push({ t, y });
return obj;
}, {});
Object.keys(map).forEach(key => {
map[key] = getDateArray(map[key], startDate, endDate, unit);
});
setData(map);
}
function handleCreate(options) {
const legend = {
position: 'bottom',
};
options.legend = legend;
}
function handleUpdate(chart) {
chart.data.datasets = datasets;
chart.update();
}
useEffect(() => {
loadData();
}, [websiteId, startDate, endDate]);
if (!data) {
return null;
}
return (
<BarChart
chartId={`events-${websiteId}`}
datasets={datasets}
unit={unit}
records={getDateLength(startDate, endDate, unit)}
onCreate={handleCreate}
onUpdate={handleUpdate}
stacked
/>
);
}

View File

@ -0,0 +1,3 @@
.chart {
display: flex;
}

View File

@ -0,0 +1,37 @@
import React from 'react';
import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css';
export default function EventsTable({
websiteId,
startDate,
endDate,
limit,
onExpand,
onDataLoad,
}) {
return (
<MetricsTable
title="Events"
type="event"
metric="Actions"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
limit={limit}
renderLabel={({ x }) => <Label value={x} />}
onExpand={onExpand}
onDataLoad={onDataLoad}
/>
);
}
const Label = ({ value }) => {
const [event, label] = value.split(':');
return (
<>
<span className={styles.type}>{event}</span>
{label}
</>
);
};

View File

@ -0,0 +1,7 @@
.type {
font-size: var(--font-size-small);
padding: 2px 4px;
border: 1px solid var(--gray300);
border-radius: 4px;
margin-right: 10px;
}

View File

@ -35,7 +35,7 @@ export default function MetricsBar({ websiteId, startDate, endDate, className })
<MetricCard label="Visitors" value={uniques} format={formatFunc} /> <MetricCard label="Visitors" value={uniques} format={formatFunc} />
<MetricCard <MetricCard
label="Bounce rate" label="Bounce rate"
value={uniques ? (bounces / uniques) * 100 : 0} value={pageviews ? (bounces / pageviews) * 100 : 0}
format={n => Number(n).toFixed(0) + '%'} format={n => Number(n).toFixed(0) + '%'}
/> />
<MetricCard <MetricCard

View File

@ -8,6 +8,7 @@ import { get } from 'lib/web';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format'; import { formatNumber, formatLongNumber } from 'lib/format';
import styles from './MetricsTable.module.css'; import styles from './MetricsTable.module.css';
import Loading from '../common/Loading';
export default function MetricsTable({ export default function MetricsTable({
title, title,
@ -21,9 +22,9 @@ export default function MetricsTable({
filterOptions, filterOptions,
limit, limit,
headerComponent, headerComponent,
renderLabel,
onDataLoad = () => {}, onDataLoad = () => {},
onExpand = () => {}, onExpand = () => {},
labelRenderer = e => e,
}) { }) {
const [data, setData] = useState(); const [data, setData] = useState();
const [format, setFormat] = useState(true); const [format, setFormat] = useState(true);
@ -43,37 +44,34 @@ export default function MetricsTable({
async function loadData() { async function loadData() {
const data = await get(`/api/website/${websiteId}/rankings`, { const data = await get(`/api/website/${websiteId}/rankings`, {
type,
start_at: +startDate, start_at: +startDate,
end_at: +endDate, end_at: +endDate,
type,
}); });
setData(data); setData(data);
onDataLoad(data); onDataLoad(data);
} }
function handleSetFormat() { const handleSetFormat = () => setFormat(state => !state);
setFormat(state => !state);
}
function getRow(x, y, z) { const getRow = row => {
const { x: label, y: value, z: percent } = row;
return ( return (
<AnimatedRow <AnimatedRow
key={x} key={label}
label={x} label={renderLabel ? renderLabel(row) : label}
value={y} value={value}
percent={z} percent={percent}
animate={shouldAnimate} animate={shouldAnimate}
format={formatFunc} format={formatFunc}
onClick={handleSetFormat} onClick={handleSetFormat}
labelRenderer={labelRenderer}
/> />
); );
} };
const Row = ({ index, style }) => { const Row = ({ index, style }) => {
const { x, y, z } = rankings[index]; return <div style={style}>{getRow(rankings[index])}</div>;
return <div style={style}>{getRow(x, y, z)}</div>;
}; };
useEffect(() => { useEffect(() => {
@ -82,40 +80,42 @@ export default function MetricsTable({
} }
}, [websiteId, startDate, endDate, type]); }, [websiteId, startDate, endDate, type]);
if (!data) {
return null;
}
return ( return (
<div className={classNames(styles.container, className)}> <div className={classNames(styles.container, className)}>
<div className={styles.header}> {data ? (
<div className={styles.title}>{title}</div> <>
{headerComponent} <div className={styles.header}>
<div className={styles.metric} onClick={handleSetFormat}> <div className={styles.title}>{title}</div>
{metric} {headerComponent}
</div> <div className={styles.metric} onClick={handleSetFormat}>
</div> {metric}
<div className={styles.body}> </div>
{limit ? ( </div>
rankings.map(({ x, y, z }) => getRow(x, y, z)) <div className={styles.body}>
) : ( {limit
<FixedSizeList height={600} itemCount={rankings.length} itemSize={30}> ? rankings.map(row => getRow(row))
{Row} : data?.length > 0 && (
</FixedSizeList> <FixedSizeList height={500} itemCount={rankings.length} itemSize={30}>
)} {Row}
</div> </FixedSizeList>
<div className={styles.footer}> )}
{limit && data.length > limit && ( </div>
<Button icon={<Arrow />} size="xsmall" onClick={() => onExpand(type)}> <div className={styles.footer}>
<div>More</div> {limit && data.length > limit && (
</Button> <Button icon={<Arrow />} size="xsmall" onClick={() => onExpand(type)}>
)} <div>More</div>
</div> </Button>
)}
</div>
</>
) : (
<Loading />
)}
</div> </div>
); );
} }
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labelRenderer }) => { const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
const props = useSpring({ const props = useSpring({
width: percent, width: percent,
y: value, y: value,
@ -125,7 +125,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labe
return ( return (
<div className={styles.row}> <div className={styles.row}>
<div className={styles.label}>{labelRenderer(decodeURI(label))}</div> <div className={styles.label}>{label}</div>
<div className={styles.value} onClick={onClick}> <div className={styles.value} onClick={onClick}>
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div> <animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
</div> </div>

View File

@ -25,6 +25,7 @@ export default function PagesTable({
limit={limit} limit={limit}
dataFilter={urlFilter} dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }} filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }}
renderLabel={({ x }) => decodeURI(x)}
onExpand={onExpand} onExpand={onExpand}
/> />
); );

View File

@ -1,67 +1,30 @@
import React, { useState, useRef, useEffect, useCallback } from 'react'; import React from 'react';
import ReactTooltip from 'react-tooltip'; import CheckVisible from 'components/helpers/CheckVisible';
import classNames from 'classnames'; import BarChart from './BarChart';
import ChartJS from 'chart.js';
import { format } from 'date-fns';
import styles from './PageviewsChart.module.css';
export default function PageviewsChart({ export default function PageviewsChart({ websiteId, data, unit, className }) {
websiteId, const handleUpdate = chart => {
data, const {
unit, data: { datasets },
animationDuration = 300, } = chart;
className,
children,
}) {
const canvas = useRef();
const chart = useRef();
const [tooltip, setTooltip] = useState({});
const renderLabel = useCallback( datasets[0].data = data.uniques;
(label, index, values) => { datasets[1].data = data.pageviews;
const d = new Date(values[index].value);
const n = data.pageviews.length;
switch (unit) { chart.update();
case 'day':
if (n >= 15) {
return index % ~~(n / 15) === 0 ? format(d, 'MMM d') : '';
}
return format(d, 'EEE M/d');
case 'month':
return format(d, 'MMMM');
default:
return label;
}
},
[unit, data],
);
const renderTooltip = model => {
const { opacity, title, body, labelColors } = model;
if (!opacity) {
setTooltip(null);
} else {
const [label, value] = body[0].lines[0].split(':');
setTooltip({
title: title[0],
value,
label,
labelColor: labelColors[0].backgroundColor,
});
}
}; };
function draw() { if (!data) {
if (!canvas.current) return; return null;
}
if (!chart.current) { return (
chart.current = new ChartJS(canvas.current, { <CheckVisible>
type: 'bar', {visible => (
data: { <BarChart
datasets: [ className={className}
chartId={websiteId}
datasets={[
{ {
label: 'unique visitors', label: 'unique visitors',
data: data.uniques, data: data.uniques,
@ -78,97 +41,13 @@ export default function PageviewsChart({
borderColor: 'rgb(13, 102, 208, 0.2)', borderColor: 'rgb(13, 102, 208, 0.2)',
borderWidth: 1, borderWidth: 1,
}, },
], ]}
}, unit={unit}
options: { records={data.pageviews.length}
animation: { animationDuration={visible ? 300 : 0}
duration: animationDuration, onUpdate={handleUpdate}
}, />
tooltips: { )}
enabled: false, </CheckVisible>
custom: renderTooltip,
},
hover: {
animationDuration: 0,
},
scales: {
xAxes: [
{
type: 'time',
distribution: 'series',
time: {
unit,
tooltipFormat: 'ddd MMMM DD YYYY',
},
ticks: {
callback: renderLabel,
maxRotation: 0,
},
gridLines: {
display: false,
},
offset: true,
stacked: true,
},
],
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
},
});
} else {
const {
data: { datasets },
options,
} = chart.current;
datasets[0].data = data.uniques;
datasets[1].data = data.pageviews;
options.scales.xAxes[0].time.unit = unit;
options.scales.xAxes[0].ticks.callback = renderLabel;
options.animation.duration = animationDuration;
chart.current.update();
}
}
useEffect(() => {
if (data) {
draw();
setTooltip(null);
}
}, [data]);
return (
<div
data-tip=""
data-for={`${websiteId}-tooltip`}
className={classNames(styles.chart, className)}
>
<canvas ref={canvas} width={960} height={400} />
<ReactTooltip id={`${websiteId}-tooltip`}>
{tooltip ? <Tooltip {...tooltip} /> : null}
</ReactTooltip>
{children}
</div>
); );
} }
const Tooltip = ({ title, value, label, labelColor }) => (
<div className={styles.tooltip}>
<div className={styles.content}>
<div className={styles.title}>{title}</div>
<div className={styles.metric}>
<div className={styles.dot}>
<div className={styles.color} style={{ backgroundColor: labelColor }} />
</div>
{value} {label}
</div>
</div>
</div>
);

View File

@ -13,13 +13,13 @@ export default function Referrers({
}) { }) {
const [filter, setFilter] = useState('Combined'); const [filter, setFilter] = useState('Combined');
const renderLink = url => { const renderLink = ({ x: url }) => {
return url.startsWith('http') ? ( return url.startsWith('http') ? (
<a href={url} target="_blank" rel="noreferrer"> <a href={url} target="_blank" rel="noreferrer">
{url} {decodeURI(url)}
</a> </a>
) : ( ) : (
url decodeURI(url)
); );
}; };
@ -40,7 +40,7 @@ export default function Referrers({
raw: filter === 'Raw', raw: filter === 'Raw',
}} }}
onExpand={onExpand} onExpand={onExpand}
labelRenderer={renderLink} renderLabel={renderLink}
/> />
); );
} }

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect, useMemo, useRef } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import PageviewsChart from './PageviewsChart'; import PageviewsChart from './PageviewsChart';
import CheckVisible from '../helpers/CheckVisible';
import MetricsBar from './MetricsBar'; import MetricsBar from './MetricsBar';
import QuickButtons from './QuickButtons'; import QuickButtons from './QuickButtons';
import DateFilter from '../common/DateFilter'; import DateFilter from '../common/DateFilter';
@ -74,18 +73,10 @@ export default function WebsiteChart({
</StickyHeader> </StickyHeader>
</div> </div>
<div className="row"> <div className="row">
<CheckVisible className="col"> <div className="col">
{visible => ( <PageviewsChart websiteId={websiteId} data={{ pageviews, uniques }} unit={unit} />
<PageviewsChart <QuickButtons value={value} onChange={handleDateChange} />
websiteId={websiteId} </div>
data={{ pageviews, uniques }}
unit={unit}
animationDuration={visible ? 300 : 0}
>
<QuickButtons value={value} onChange={handleDateChange} />
</PageviewsChart>
)}
</CheckVisible>
</div> </div>
</> </>
); );

View File

@ -98,6 +98,12 @@ export function getDateArray(data, startDate, endDate, unit) {
function findData(t) { function findData(t) {
const x = data.find(e => { const x = data.find(e => {
console.log(
new Date(e.t),
getLocalTime(new Date(e.t)),
getLocalTime(new Date(e.t)).getTime(),
normalize(new Date(t)).getTime(),
);
return getLocalTime(new Date(e.t)).getTime() === normalize(new Date(t)).getTime(); return getLocalTime(new Date(e.t)).getTime() === normalize(new Date(t)).getTime();
}); });
@ -108,8 +114,13 @@ export function getDateArray(data, startDate, endDate, unit) {
const t = add(startDate, i); const t = add(startDate, i);
const y = findData(t); const y = findData(t);
arr.push({ t, y }); arr.push({ ...data[i], t, y });
} }
return arr; return arr;
} }
export function getDateLength(startDate, endDate, unit) {
const [diff] = dateFuncs[unit];
return diff(endDate, startDate) + 1;
}

View File

@ -35,5 +35,6 @@ export default prisma;
export async function runQuery(query) { export async function runQuery(query) {
return query.catch(e => { return query.catch(e => {
console.error(e); console.error(e);
throw e;
}); });
} }

View File

@ -49,12 +49,13 @@ export const urlFilter = (data, { domain, raw }) => {
}; };
export const refFilter = (data, { domain, domainOnly, raw }) => { export const refFilter = (data, { domain, domainOnly, raw }) => {
const regex = new RegExp(domain.startsWith('http') ? domain : `http[s]?://${domain}`);
const isValidRef = ref => { const isValidRef = ref => {
return ref !== '' && !ref.startsWith('/') && !ref.startsWith('#'); return ref !== '' && !ref.startsWith('/') && !ref.startsWith('#');
}; };
if (raw) { if (raw) {
const regex = new RegExp(`http[s]?://([^.]+.)?${domain}`);
return data.filter(({ x }) => isValidRef(x) && !regex.test(x)); return data.filter(({ x }) => isValidRef(x) && !regex.test(x));
} }
@ -62,7 +63,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => {
try { try {
const { hostname, origin, pathname, searchParams, protocol } = new URL(url); const { hostname, origin, pathname, searchParams, protocol } = new URL(url);
if (hostname === domain) { if (hostname === domain || regex.test(url)) {
return null; return null;
} }
@ -124,5 +125,5 @@ export const countryFilter = data =>
export const percentFilter = data => { export const percentFilter = data => {
const total = data.reduce((n, { y }) => n + y, 0); const total = data.reduce((n, { y }) => n + y, 0);
return data.map(({ x, y }) => ({ x, y, z: total ? (y / total) * 100 : 0 })); return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props }));
}; };

View File

@ -5,10 +5,28 @@ import { subMinutes } from 'date-fns';
const POSTGRESQL = 'postgresql'; const POSTGRESQL = 'postgresql';
const MYSQL = 'mysql'; const MYSQL = 'mysql';
const DATE_FORMATS = {
minute: '%Y-%m-%d %H:%i:00',
hour: '%Y-%m-%d %H:00:00',
day: '%Y-%m-%d',
month: '%Y-%m-01',
year: '%Y-01-01',
};
export function getDatabase() { export function getDatabase() {
return process.env.DATABASE_TYPE || process.env.DATABASE_URL.split(':')[0]; return process.env.DATABASE_TYPE || process.env.DATABASE_URL.split(':')[0];
} }
export function getDateQuery(field, unit, timezone) {
if (timezone) {
const tz = moment.tz(timezone).format('Z');
return `DATE_FORMAT(convert_tz(${field},'+00:00','${tz}'), '${DATE_FORMATS[unit]}')`;
}
return `DATE_FORMAT(${field}, '${DATE_FORMATS[unit]}')`;
}
export async function getWebsiteById(website_id) { export async function getWebsiteById(website_id) {
return runQuery( return runQuery(
prisma.website.findOne({ prisma.website.findOne({
@ -236,7 +254,7 @@ export function getMetrics(website_id, start_at, end_at) {
` `
select sum(t.c) as "pageviews", select sum(t.c) as "pageviews",
count(distinct t.session_id) as "uniques", count(distinct t.session_id) as "uniques",
sum(case when t.c = 1 then t.c else 0 end) as "bounces", sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(t.time) as "totaltime" sum(t.time) as "totaltime"
from ( from (
select session_id, select session_id,
@ -260,11 +278,11 @@ export function getMetrics(website_id, start_at, end_at) {
` `
select sum(t.c) as "pageviews", select sum(t.c) as "pageviews",
count(distinct t.session_id) as "uniques", count(distinct t.session_id) as "uniques",
sum(case when t.c = 1 then t.c else 0 end) as "bounces", sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(t.time) as "totaltime" sum(t.time) as "totaltime"
from ( from (
select session_id, select session_id,
date_trunc('hour', created_at), ${getDateQuery('created_at', 'hour')},
count(*) c, count(*) c,
floor(unix_timestamp(max(created_at)) - unix_timestamp(min(created_at))) as "time" floor(unix_timestamp(max(created_at)) - unix_timestamp(min(created_at))) as "time"
from pageview from pageview
@ -296,7 +314,7 @@ export function getPageviews(
return prisma.$queryRaw( return prisma.$queryRaw(
` `
select date_trunc('${unit}', created_at at time zone '${timezone}') t, select date_trunc('${unit}', created_at at time zone '${timezone}') t,
count(${count}) y count(${count}) y
from pageview from pageview
where website_id=$1 where website_id=$1
and created_at between $2 and $3 and created_at between $2 and $3
@ -310,11 +328,10 @@ export function getPageviews(
} }
if (db === MYSQL) { if (db === MYSQL) {
const tz = moment.tz(timezone).format('Z');
return prisma.$queryRaw( return prisma.$queryRaw(
` `
select date_trunc('${unit}', convert_tz(created_at,'+00:00','${tz}')) t, select ${getDateQuery('created_at', unit, timezone)} t,
count(${count}) y count(${count}) y
from pageview from pageview
where website_id=? where website_id=?
and created_at between ? and ? and created_at between ? and ?
@ -400,3 +417,47 @@ export function getActiveVisitors(website_id) {
return Promise.resolve([]); return Promise.resolve([]);
} }
export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit = 'day') {
const db = getDatabase();
if (db === POSTGRESQL) {
return prisma.$queryRaw(
`
select
event_value x,
date_trunc('${unit}', created_at at time zone '${timezone}') t,
count(*) y
from event
where website_id=$1
and created_at between $2 and $3
group by 1, 2
order by 2
`,
website_id,
start_at,
end_at,
);
}
if (db === MYSQL) {
return prisma.$queryRaw(
`
select
event_value x,
${getDateQuery('created_at', unit, timezone)} t,
count(*) y
from event
where website_id=?
and created_at between ? and ?
group by 1, 2
order by 2
`,
website_id,
start_at,
end_at,
);
}
return Promise.resolve([]);
}

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "0.16.3", "version": "0.17.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ", "description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
@ -45,7 +45,7 @@
"classnames": "^2.2.6", "classnames": "^2.2.6",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^2.15.0", "date-fns": "^2.16.0",
"date-fns-tz": "^1.0.10", "date-fns-tz": "^1.0.10",
"detect-browser": "^5.1.1", "detect-browser": "^5.1.1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
@ -62,7 +62,7 @@
"react-redux": "^7.2.1", "react-redux": "^7.2.1",
"react-simple-maps": "^2.1.2", "react-simple-maps": "^2.1.2",
"react-spring": "^8.0.27", "react-spring": "^8.0.27",
"react-tooltip": "^4.2.8", "react-tooltip": "^4.2.9",
"react-window": "^1.8.5", "react-window": "^1.8.5",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
@ -86,14 +86,14 @@
"eslint-plugin-react": "^7.20.6", "eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.0", "eslint-plugin-react-hooks": "^4.1.0",
"husky": "^4.2.5", "husky": "^4.2.5",
"lint-staged": "^10.2.9", "lint-staged": "^10.2.13",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss-flexbugs-fixes": "^4.2.1", "postcss-flexbugs-fixes": "^4.2.1",
"postcss-import": "^12.0.1", "postcss-import": "^12.0.1",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.0.5", "prettier": "^2.1.1",
"prettier-eslint": "^11.0.0", "prettier-eslint": "^11.0.0",
"rollup": "^2.26.5", "rollup": "^2.26.6",
"rollup-plugin-hashbang": "^2.2.2", "rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"stylelint": "^13.6.0", "stylelint": "^13.6.0",

View File

@ -0,0 +1,21 @@
import moment from 'moment-timezone';
import { getEvents } from 'lib/queries';
import { ok, badRequest } from 'lib/response';
const unitTypes = ['month', 'hour', 'day'];
export default async (req, res) => {
const { id, start_at, end_at, unit, tz } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res);
}
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
const events = await getEvents(websiteId, startDate, endDate, tz, unit);
return ok(res, events);
};

View File

@ -11,12 +11,13 @@ export default async (req, res) => {
return badRequest(res); return badRequest(res);
} }
const start = new Date(+start_at); const websiteId = +id;
const end = new Date(+end_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
const [pageviews, uniques] = await Promise.all([ const [pageviews, uniques] = await Promise.all([
getPageviews(+id, start, end, tz, unit, '*'), getPageviews(websiteId, startDate, endDate, tz, unit, '*'),
getPageviews(+id, start, end, tz, unit, 'distinct session_id'), getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'),
]); ]);
return ok(res, { pageviews, uniques }); return ok(res, { pageviews, uniques });

View File

@ -4,16 +4,42 @@ import { ok, badRequest } from 'lib/response';
const sessionColumns = ['browser', 'os', 'device', 'country']; const sessionColumns = ['browser', 'os', 'device', 'country'];
const pageviewColumns = ['url', 'referrer']; const pageviewColumns = ['url', 'referrer'];
function getTable(type) {
if (type === 'event') {
return 'event';
}
if (sessionColumns.includes(type)) {
return 'session';
}
return 'pageview';
}
function getColumn(type) {
if (type === 'event') {
return `concat(event_type, ':', event_value)`;
}
return type;
}
export default async (req, res) => { export default async (req, res) => {
const { id, type, start_at, end_at } = req.query; const { id, type, start_at, end_at } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
if (!sessionColumns.includes(type) && !pageviewColumns.includes(type)) { if (type !== 'event' && !sessionColumns.includes(type) && !pageviewColumns.includes(type)) {
return badRequest(res); return badRequest(res);
} }
const table = sessionColumns.includes(type) ? 'session' : 'pageview'; const rankings = await getRankings(
websiteId,
const rankings = await getRankings(+id, new Date(+start_at), new Date(+end_at), type, table); startDate,
endDate,
getColumn(type),
getTable(type),
);
return ok(res, rankings); return ok(res, rankings);
}; };

View File

@ -3,7 +3,6 @@ drop table if exists pageview;
drop table if exists session; drop table if exists session;
drop table if exists website; drop table if exists website;
drop table if exists account; drop table if exists account;
drop function if exists date_trunc;
create table account ( create table account (
user_id int unsigned not null auto_increment primary key, user_id int unsigned not null auto_increment primary key,
@ -71,37 +70,11 @@ create index session_website_id_idx on session(website_id);
create index pageview_created_at_idx on pageview(created_at); create index pageview_created_at_idx on pageview(created_at);
create index pageview_website_id_idx on pageview(website_id); create index pageview_website_id_idx on pageview(website_id);
create index pageview_session_id_idx on pageview(session_id); create index pageview_session_id_idx on pageview(session_id);
create index pageview_website_id_created_at_idx on pageview(website_id, created_at);
create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at);
create index event_created_at_idx on event(created_at); create index event_created_at_idx on event(created_at);
create index event_website_id_idx on event(website_id); create index event_website_id_idx on event(website_id);
create index event_session_id_idx on event(session_id); create index event_session_id_idx on event(session_id);
delimiter $$
create function date_trunc(
in_granularity enum('minute', 'hour', 'day', 'month', 'year'),
in_datetime datetime(6)
)
returns datetime(6)
deterministic
begin
if (in_granularity = 'minute') then
return DATE_FORMAT(in_datetime, '%Y-%m-%d %H:%i:00.0000');
end if;
if (in_granularity = 'hour') then
return DATE_FORMAT(in_datetime, '%Y-%m-%d %H:00:00.0000');
end if;
if (in_granularity = 'day') then
return DATE_FORMAT(in_datetime, '%Y-%m-%d 00:00:00.0000');
end if;
if (in_granularity = 'month') then
return DATE_FORMAT(in_datetime, '%Y-%m-01 00:00:00.0000');
end if;
if (in_granularity = 'year') then
return DATE_FORMAT(in_datetime, '%Y-01-01 00:00:00.0000');
end if;
end;
$$
insert into account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true); insert into account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);

View File

@ -64,6 +64,8 @@ create index session_website_id_idx on session(website_id);
create index pageview_created_at_idx on pageview(created_at); create index pageview_created_at_idx on pageview(created_at);
create index pageview_website_id_idx on pageview(website_id); create index pageview_website_id_idx on pageview(website_id);
create index pageview_session_id_idx on pageview(session_id); create index pageview_session_id_idx on pageview(session_id);
create index pageview_website_id_created_at_idx on pageview(website_id, created_at);
create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at);
create index event_created_at_idx on event(created_at); create index event_created_at_idx on event(created_at);
create index event_website_id_idx on event(website_id); create index event_website_id_idx on event(website_id);

186
yarn.lock
View File

@ -1086,7 +1086,7 @@
levenary "^1.1.1" levenary "^1.1.1"
semver "^5.5.0" semver "^5.5.0"
"@babel/preset-modules@0.1.3", "@babel/preset-modules@^0.1.3": "@babel/preset-modules@0.1.3":
version "0.1.3" version "0.1.3"
resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72"
integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg== integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==
@ -1097,6 +1097,17 @@
"@babel/types" "^7.4.4" "@babel/types" "^7.4.4"
esutils "^2.0.2" esutils "^2.0.2"
"@babel/preset-modules@^0.1.3":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e"
integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
"@babel/plugin-transform-dotall-regex" "^7.4.4"
"@babel/types" "^7.4.4"
esutils "^2.0.2"
"@babel/preset-react@7.9.4": "@babel/preset-react@7.9.4":
version "7.9.4" version "7.9.4"
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.9.4.tgz#c6c97693ac65b6b9c0b4f25b948a8f665463014d" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.9.4.tgz#c6c97693ac65b6b9c0b4f25b948a8f665463014d"
@ -1458,9 +1469,9 @@
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
"@types/node@*": "@types/node@*":
version "14.6.0" version "14.6.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.1.tgz#fdf6f6c6c73d3d8eee9c98a9a0485bc524b048d7"
integrity sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA== integrity sha512-HnYlg/BRF8uC1FyKRFZwRaCPTPYKa+6I8QiUZFLredaGOou481cgFS4wKRFyKvQtX8xudqkSdBczJHIYSQYKrQ==
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.0" version "2.4.0"
@ -1489,40 +1500,40 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
"@typescript-eslint/experimental-utils@3.9.1": "@typescript-eslint/experimental-utils@3.10.1":
version "3.9.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.9.1.tgz#b140b2dc7a7554a44f8a86fb6fe7cbfe57ca059e" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686"
integrity sha512-lkiZ8iBBaYoyEKhCkkw4SAeatXyBq9Ece5bZXdLe1LWBUwTszGbmbiqmQbwWA8cSYDnjWXp9eDbXpf9Sn0hLAg== integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==
dependencies: dependencies:
"@types/json-schema" "^7.0.3" "@types/json-schema" "^7.0.3"
"@typescript-eslint/types" "3.9.1" "@typescript-eslint/types" "3.10.1"
"@typescript-eslint/typescript-estree" "3.9.1" "@typescript-eslint/typescript-estree" "3.10.1"
eslint-scope "^5.0.0" eslint-scope "^5.0.0"
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
"@typescript-eslint/parser@^3.0.0": "@typescript-eslint/parser@^3.0.0":
version "3.9.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.9.1.tgz#ab7983abaea0ae138ff5671c7c7739d8a191b181" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.1.tgz#1883858e83e8b442627e1ac6f408925211155467"
integrity sha512-y5QvPFUn4Vl4qM40lI+pNWhTcOWtpZAJ8pOEQ21fTTW4xTJkRplMjMRje7LYTXqVKKX9GJhcyweMz2+W1J5bMg== integrity sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==
dependencies: dependencies:
"@types/eslint-visitor-keys" "^1.0.0" "@types/eslint-visitor-keys" "^1.0.0"
"@typescript-eslint/experimental-utils" "3.9.1" "@typescript-eslint/experimental-utils" "3.10.1"
"@typescript-eslint/types" "3.9.1" "@typescript-eslint/types" "3.10.1"
"@typescript-eslint/typescript-estree" "3.9.1" "@typescript-eslint/typescript-estree" "3.10.1"
eslint-visitor-keys "^1.1.0" eslint-visitor-keys "^1.1.0"
"@typescript-eslint/types@3.9.1": "@typescript-eslint/types@3.10.1":
version "3.9.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.9.1.tgz#b2a6eaac843cf2f2777b3f2464fb1fbce5111416" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
integrity sha512-15JcTlNQE1BsYy5NBhctnEhEoctjXOjOK+Q+rk8ugC+WXU9rAcS2BYhoh6X4rOaXJEpIYDl+p7ix+A5U0BqPTw== integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==
"@typescript-eslint/typescript-estree@3.9.1": "@typescript-eslint/typescript-estree@3.10.1":
version "3.9.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.9.1.tgz#fd81cada74bc8a7f3a2345b00897acb087935779" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853"
integrity sha512-IqM0gfGxOmIKPhiHW/iyAEXwSVqMmR2wJ9uXHNdFpqVvPaQ3dWg302vW127sBpAiqM9SfHhyS40NKLsoMpN2KA== integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==
dependencies: dependencies:
"@typescript-eslint/types" "3.9.1" "@typescript-eslint/types" "3.10.1"
"@typescript-eslint/visitor-keys" "3.9.1" "@typescript-eslint/visitor-keys" "3.10.1"
debug "^4.1.1" debug "^4.1.1"
glob "^7.1.6" glob "^7.1.6"
is-glob "^4.0.1" is-glob "^4.0.1"
@ -1530,10 +1541,10 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@typescript-eslint/visitor-keys@3.9.1": "@typescript-eslint/visitor-keys@3.10.1":
version "3.9.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.9.1.tgz#92af3747cdb71509199a8f7a4f00b41d636551d1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931"
integrity sha512-zxdtUjeoSh+prCpogswMwVUJfEFmCOjdzK9rpNjNBfm6EyPt99x3RrJoBOGZO23FCt0WPKUCOL5mb/9D5LjdwQ== integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==
dependencies: dependencies:
eslint-visitor-keys "^1.1.0" eslint-visitor-keys "^1.1.0"
@ -2352,9 +2363,9 @@ camelcase@^6.0.0:
integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111: caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111:
version "1.0.30001117" version "1.0.30001119"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001117.tgz#69a9fae5d480eaa9589f7641a83842ad396d17c4" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001119.tgz#99185d04bc00e76a86c9ff731dc5ec8e53aefca1"
integrity sha512-4tY0Fatzdx59kYjQs+bNxUwZB03ZEBgVmJ1UkFPz/Q8OLiUUbjct2EdpnXj0fvFTPej2EkbPIG0w8BWsjAyk1Q== integrity sha512-Hpwa4obv7EGP+TjkCh/wVvbtNJewxmtg4yVJBLFnxo35vbPapBr138bUWENkb5j5L9JZJ9RXLn4OrXRG/cecPQ==
ccount@^1.0.0: ccount@^1.0.0:
version "1.0.5" version "1.0.5"
@ -2536,7 +2547,7 @@ cli-cursor@^3.1.0:
dependencies: dependencies:
restore-cursor "^3.1.0" restore-cursor "^3.1.0"
cli-truncate@2.1.0, cli-truncate@^2.1.0: cli-truncate@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
@ -2637,10 +2648,10 @@ commander@2, commander@^2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^5.1.0: commander@^6.0.0:
version "5.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA==
common-tags@^1.4.0: common-tags@^1.4.0:
version "1.8.0" version "1.8.0"
@ -2765,6 +2776,17 @@ cosmiconfig@^6.0.0:
path-type "^4.0.0" path-type "^4.0.0"
yaml "^1.7.2" yaml "^1.7.2"
cosmiconfig@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==
dependencies:
"@types/parse-json" "^4.0.0"
import-fresh "^3.2.1"
parse-json "^5.0.0"
path-type "^4.0.0"
yaml "^1.10.0"
create-ecdh@^4.0.0: create-ecdh@^4.0.0:
version "4.0.4" version "4.0.4"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@ -3092,10 +3114,10 @@ date-fns-tz@^1.0.10:
resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.0.10.tgz#30fef0038f80534fddd8e133a6b8ca55ba313748" resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.0.10.tgz#30fef0038f80534fddd8e133a6b8ca55ba313748"
integrity sha512-cHQAz0/9uDABaUNDM80Mj1FL4ODlxs1xEY4b0DQuAooO2UdNKvDkNbV8ogLnxLbv02Ru1HXFcot0pVvDRBgptg== integrity sha512-cHQAz0/9uDABaUNDM80Mj1FL4ODlxs1xEY4b0DQuAooO2UdNKvDkNbV8ogLnxLbv02Ru1HXFcot0pVvDRBgptg==
date-fns@^2.15.0: date-fns@^2.16.0:
version "2.15.0" version "2.16.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.0.tgz#d34f0f5f2fd498c984513042e8f7247ea86c4cb7"
integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ== integrity sha512-DWTRyfOA85sZ4IiXPHhiRIOs3fW5U6Msrp+gElXARa6EpoQTXPyHQmh7hr+ssw2nx9FtOQWnAMJKgL5vaJqILw==
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
version "4.1.1" version "4.1.1"
@ -3351,9 +3373,9 @@ duplexify@^3.4.2, duplexify@^3.6.0:
stream-shift "^1.0.0" stream-shift "^1.0.0"
electron-to-chromium@^1.3.488, electron-to-chromium@^1.3.523: electron-to-chromium@^1.3.488, electron-to-chromium@^1.3.523:
version "1.3.544" version "1.3.553"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.544.tgz#ac1f7d319f6060f3d6d122261d542ec77eb1427e" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.553.tgz#c4693d8660470a3aa830907890e446a9f3b26376"
integrity sha512-jx6H7M1db76Q/dI3MadZC4qwNTvpiq8tdYEJswxexrIm5bH+LKRdg+VAteMF1tJJbBLrcuogE9N3nxT3Dp1gag== integrity sha512-wi/hoMuTGK6OJoLOHqmXFA9BWOQGF2nInCfk+/Owhd4VVfuenKE2LZr9TtFCmwyda2SE9hG+sRnqRCwhYgFeIg==
elliptic@^6.5.3: elliptic@^6.5.3:
version "6.5.3" version "6.5.3"
@ -3404,7 +3426,7 @@ enhanced-resolve@^4.3.0:
memory-fs "^0.5.0" memory-fs "^0.5.0"
tapable "^1.0.0" tapable "^1.0.0"
enquirer@^2.3.5: enquirer@^2.3.5, enquirer@^2.3.6:
version "2.3.6" version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
@ -3746,7 +3768,7 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4" md5.js "^1.3.4"
safe-buffer "^5.1.1" safe-buffer "^5.1.1"
execa@^4.0.1: execa@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2" resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2"
integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A== integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==
@ -4435,11 +4457,11 @@ ignore@^5.1.4, ignore@^5.1.8:
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
immer@^7.0.3: immer@^7.0.3:
version "7.0.7" version "7.0.8"
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.7.tgz#9dfe713d49bf871cc59aedfce59b1992fa37a977" resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.8.tgz#41dcbc5669a76500d017bef3ad0d03ce0a1d7c1e"
integrity sha512-Q8yYwVADJXrNfp1ZUAh4XDHkcoE3wpdpb4mC5abDSajs2EbW8+cGdPyAnglMyLnm7EF6ojD2xBFX7L5i4TIytw== integrity sha512-XnpIN8PXBBaOD43U8Z17qg6RQiKQYGDGGCIbz1ixmLGwBkSWwmrmx5X7d+hTtXDM8ur7m5OdLE0PiO+y5RB3pw==
import-fresh@^3.0.0, import-fresh@^3.1.0: import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
@ -4999,20 +5021,20 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
lint-staged@^10.2.9: lint-staged@^10.2.13:
version "10.2.11" version "10.2.13"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.2.11.tgz#713c80877f2dc8b609b05bc59020234e766c9720" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.2.13.tgz#b9c504683470edfc464b7d3fe3845a5a1efcd814"
integrity sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA== integrity sha512-conwlukNV6aL9SiMWjFtDp5exeDnTMekdNPDZsKGnpfQuHcO0E3L3Bbf58lcR+M7vk6LpCilxDAVks/DDVBYlA==
dependencies: dependencies:
chalk "^4.0.0" chalk "^4.1.0"
cli-truncate "2.1.0" cli-truncate "^2.1.0"
commander "^5.1.0" commander "^6.0.0"
cosmiconfig "^6.0.0" cosmiconfig "^7.0.0"
debug "^4.1.1" debug "^4.1.1"
dedent "^0.7.0" dedent "^0.7.0"
enquirer "^2.3.5" enquirer "^2.3.6"
execa "^4.0.1" execa "^4.0.3"
listr2 "^2.1.0" listr2 "^2.6.0"
log-symbols "^4.0.0" log-symbols "^4.0.0"
micromatch "^4.0.2" micromatch "^4.0.2"
normalize-path "^3.0.0" normalize-path "^3.0.0"
@ -5020,10 +5042,10 @@ lint-staged@^10.2.9:
string-argv "0.3.1" string-argv "0.3.1"
stringify-object "^3.3.0" stringify-object "^3.3.0"
listr2@^2.1.0: listr2@^2.6.0:
version "2.6.0" version "2.6.1"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.6.0.tgz#788a3d202978a1b8582062952cbc49272c8e206a" resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.6.1.tgz#fbbabd8eea723924df7530042c1990b346e81706"
integrity sha512-nwmqTJYQQ+AsKb4fCXH/6/UmLCEDL1jkRAdSn9M6cEUzoRGrs33YD/3N86gAZQnGZ6hxV18XSdlBcJ1GTmetJA== integrity sha512-1aPX9GkS+W0aHfPUDedJqeqj0DOe1605NaNoqdwEYw/UF2UbZgCIIMpXXZALeG/8xzwMBztguzQEubU5Xw1Qbw==
dependencies: dependencies:
chalk "^4.1.0" chalk "^4.1.0"
cli-truncate "^2.1.0" cli-truncate "^2.1.0"
@ -5164,9 +5186,9 @@ loglevel-colored-level-prefix@^1.0.0:
loglevel "^1.4.1" loglevel "^1.4.1"
loglevel@^1.4.1: loglevel@^1.4.1:
version "1.6.8" version "1.7.0"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0"
integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==
longest-streak@^2.0.1: longest-streak@^2.0.1:
version "2.0.4" version "2.0.4"
@ -6779,10 +6801,10 @@ prettier-linter-helpers@^1.0.0:
dependencies: dependencies:
fast-diff "^1.1.2" fast-diff "^1.1.2"
prettier@^2.0.0, prettier@^2.0.5: prettier@^2.0.0, prettier@^2.1.1:
version "2.0.5" version "2.1.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6"
integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==
pretty-format@^23.0.1: pretty-format@^23.0.1:
version "23.6.0" version "23.6.0"
@ -6999,10 +7021,10 @@ react-spring@^8.0.27:
"@babel/runtime" "^7.3.1" "@babel/runtime" "^7.3.1"
prop-types "^15.5.8" prop-types "^15.5.8"
react-tooltip@^4.2.8: react-tooltip@^4.2.9:
version "4.2.8" version "4.2.9"
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.8.tgz#270858fee46fab73b66de316271aa94145f7446b" resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.9.tgz#0dd08d14191f5d0e56b51c822fa20c2d81a24272"
integrity sha512-pDWa0/khTAgIfldp95tHgyuYyBhWNlfaU2LF9ubAKxpoqNe15uyf+uLlnhK/Lstb6FU8E8/SL28Wp6oEO9xw3g== integrity sha512-DgZyg5oxk9/orgePDLLeuDtlwwYv7CalJRahk9nNsoEJDzIO58GC6zSAet4bKTm6c01hg1z3EocP9H0nmMHTMA==
dependencies: dependencies:
prop-types "^15.7.2" prop-types "^15.7.2"
uuid "^7.0.3" uuid "^7.0.3"
@ -7417,10 +7439,10 @@ rollup-plugin-terser@^7.0.0:
serialize-javascript "^4.0.0" serialize-javascript "^4.0.0"
terser "^5.0.0" terser "^5.0.0"
rollup@^2.26.5: rollup@^2.26.6:
version "2.26.5" version "2.26.6"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.26.5.tgz#5562ec36fcba3eed65cfd630bd78e037ad0e0307" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.26.6.tgz#0b460c1da224c6af12a1e948a28c513aa11f2b93"
integrity sha512-rCyFG3ZtQdnn9YwfuAVH0l/Om34BdO5lwCA0W6Hq+bNB21dVEBbCRxhaHOmu1G7OBFDWytbzAC104u7rxHwGjA== integrity sha512-iSB7eE3k/VNQHnI7ckS++4yIqTamoUCB1xo7MswhJ/fg22oFYR5+xCrUZVviBj97jvc5A31MPbVMw1Wc3jWxmw==
optionalDependencies: optionalDependencies:
fsevents "~2.1.2" fsevents "~2.1.2"
@ -8986,7 +9008,7 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.7.2: yaml@^1.10.0, yaml@^1.7.2:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==