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

View File

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

View File

@ -2,13 +2,13 @@
display: inline-flex;
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--gray400);
border: 1px solid var(--gray500);
}
.group .button {
border-radius: 0;
background: var(--gray50);
border-left: 1px solid var(--gray400);
border-left: 1px solid var(--gray500);
padding: 4px 8px;
}
@ -16,6 +16,10 @@
border: 0;
}
.group .button:hover {
background: var(--gray100);
}
.group .button + .button {
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 {
position: relative;
border-left: 1px solid var(--gray300);
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="Bounce rate"
value={uniques ? (bounces / uniques) * 100 : 0}
value={pageviews ? (bounces / pageviews) * 100 : 0}
format={n => Number(n).toFixed(0) + '%'}
/>
<MetricCard

View File

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

View File

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

View File

@ -1,67 +1,30 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import ChartJS from 'chart.js';
import { format } from 'date-fns';
import styles from './PageviewsChart.module.css';
import React from 'react';
import CheckVisible from 'components/helpers/CheckVisible';
import BarChart from './BarChart';
export default function PageviewsChart({
websiteId,
data,
unit,
animationDuration = 300,
className,
children,
}) {
const canvas = useRef();
const chart = useRef();
const [tooltip, setTooltip] = useState({});
export default function PageviewsChart({ websiteId, data, unit, className }) {
const handleUpdate = chart => {
const {
data: { datasets },
} = chart;
const renderLabel = useCallback(
(label, index, values) => {
const d = new Date(values[index].value);
const n = data.pageviews.length;
datasets[0].data = data.uniques;
datasets[1].data = data.pageviews;
switch (unit) {
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,
});
}
chart.update();
};
function draw() {
if (!canvas.current) return;
if (!data) {
return null;
}
if (!chart.current) {
chart.current = new ChartJS(canvas.current, {
type: 'bar',
data: {
datasets: [
return (
<CheckVisible>
{visible => (
<BarChart
className={className}
chartId={websiteId}
datasets={[
{
label: 'unique visitors',
data: data.uniques,
@ -78,97 +41,13 @@ export default function PageviewsChart({
borderColor: 'rgb(13, 102, 208, 0.2)',
borderWidth: 1,
},
],
},
options: {
animation: {
duration: animationDuration,
},
tooltips: {
enabled: false,
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>
]}
unit={unit}
records={data.pageviews.length}
animationDuration={visible ? 300 : 0}
onUpdate={handleUpdate}
/>
)}
</CheckVisible>
);
}
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 renderLink = url => {
const renderLink = ({ x: url }) => {
return url.startsWith('http') ? (
<a href={url} target="_blank" rel="noreferrer">
{url}
{decodeURI(url)}
</a>
) : (
url
decodeURI(url)
);
};
@ -40,7 +40,7 @@ export default function Referrers({
raw: filter === 'Raw',
}}
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 PageviewsChart from './PageviewsChart';
import CheckVisible from '../helpers/CheckVisible';
import MetricsBar from './MetricsBar';
import QuickButtons from './QuickButtons';
import DateFilter from '../common/DateFilter';
@ -74,18 +73,10 @@ export default function WebsiteChart({
</StickyHeader>
</div>
<div className="row">
<CheckVisible className="col">
{visible => (
<PageviewsChart
websiteId={websiteId}
data={{ pageviews, uniques }}
unit={unit}
animationDuration={visible ? 300 : 0}
>
<QuickButtons value={value} onChange={handleDateChange} />
</PageviewsChart>
)}
</CheckVisible>
<div className="col">
<PageviewsChart websiteId={websiteId} data={{ pageviews, uniques }} unit={unit} />
<QuickButtons value={value} onChange={handleDateChange} />
</div>
</div>
</>
);

View File

@ -98,6 +98,12 @@ export function getDateArray(data, startDate, endDate, unit) {
function findData(t) {
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();
});
@ -108,8 +114,13 @@ export function getDateArray(data, startDate, endDate, unit) {
const t = add(startDate, i);
const y = findData(t);
arr.push({ t, y });
arr.push({ ...data[i], t, y });
}
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) {
return query.catch(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 }) => {
const regex = new RegExp(domain.startsWith('http') ? domain : `http[s]?://${domain}`);
const isValidRef = ref => {
return ref !== '' && !ref.startsWith('/') && !ref.startsWith('#');
};
if (raw) {
const regex = new RegExp(`http[s]?://([^.]+.)?${domain}`);
return data.filter(({ x }) => isValidRef(x) && !regex.test(x));
}
@ -62,7 +63,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => {
try {
const { hostname, origin, pathname, searchParams, protocol } = new URL(url);
if (hostname === domain) {
if (hostname === domain || regex.test(url)) {
return null;
}
@ -124,5 +125,5 @@ export const countryFilter = data =>
export const percentFilter = data => {
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 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() {
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) {
return runQuery(
prisma.website.findOne({
@ -236,7 +254,7 @@ export function getMetrics(website_id, start_at, end_at) {
`
select sum(t.c) as "pageviews",
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"
from (
select session_id,
@ -260,11 +278,11 @@ export function getMetrics(website_id, start_at, end_at) {
`
select sum(t.c) as "pageviews",
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"
from (
select session_id,
date_trunc('hour', created_at),
${getDateQuery('created_at', 'hour')},
count(*) c,
floor(unix_timestamp(max(created_at)) - unix_timestamp(min(created_at))) as "time"
from pageview
@ -296,7 +314,7 @@ export function getPageviews(
return prisma.$queryRaw(
`
select date_trunc('${unit}', created_at at time zone '${timezone}') t,
count(${count}) y
count(${count}) y
from pageview
where website_id=$1
and created_at between $2 and $3
@ -310,11 +328,10 @@ export function getPageviews(
}
if (db === MYSQL) {
const tz = moment.tz(timezone).format('Z');
return prisma.$queryRaw(
`
select date_trunc('${unit}', convert_tz(created_at,'+00:00','${tz}')) t,
count(${count}) y
select ${getDateQuery('created_at', unit, timezone)} t,
count(${count}) y
from pageview
where website_id=?
and created_at between ? and ?
@ -400,3 +417,47 @@ export function getActiveVisitors(website_id) {
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",
"version": "0.16.3",
"version": "0.17.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
@ -45,7 +45,7 @@
"classnames": "^2.2.6",
"cookie": "^0.4.1",
"cors": "^2.8.5",
"date-fns": "^2.15.0",
"date-fns": "^2.16.0",
"date-fns-tz": "^1.0.10",
"detect-browser": "^5.1.1",
"dotenv": "^8.2.0",
@ -62,7 +62,7 @@
"react-redux": "^7.2.1",
"react-simple-maps": "^2.1.2",
"react-spring": "^8.0.27",
"react-tooltip": "^4.2.8",
"react-tooltip": "^4.2.9",
"react-window": "^1.8.5",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
@ -86,14 +86,14 @@
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.0",
"husky": "^4.2.5",
"lint-staged": "^10.2.9",
"lint-staged": "^10.2.13",
"npm-run-all": "^4.1.5",
"postcss-flexbugs-fixes": "^4.2.1",
"postcss-import": "^12.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.0.5",
"prettier": "^2.1.1",
"prettier-eslint": "^11.0.0",
"rollup": "^2.26.5",
"rollup": "^2.26.6",
"rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.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);
}
const start = new Date(+start_at);
const end = new Date(+end_at);
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
const [pageviews, uniques] = await Promise.all([
getPageviews(+id, start, end, tz, unit, '*'),
getPageviews(+id, start, end, tz, unit, 'distinct session_id'),
getPageviews(websiteId, startDate, endDate, tz, unit, '*'),
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'),
]);
return ok(res, { pageviews, uniques });

View File

@ -4,16 +4,42 @@ import { ok, badRequest } from 'lib/response';
const sessionColumns = ['browser', 'os', 'device', 'country'];
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) => {
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);
}
const table = sessionColumns.includes(type) ? 'session' : 'pageview';
const rankings = await getRankings(+id, new Date(+start_at), new Date(+end_at), type, table);
const rankings = await getRankings(
websiteId,
startDate,
endDate,
getColumn(type),
getTable(type),
);
return ok(res, rankings);
};

View File

@ -3,7 +3,6 @@ drop table if exists pageview;
drop table if exists session;
drop table if exists website;
drop table if exists account;
drop function if exists date_trunc;
create table account (
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_website_id_idx on pageview(website_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_website_id_idx on event(website_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);

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_website_id_idx on pageview(website_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_website_id_idx on event(website_id);

186
yarn.lock
View File

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