Merge remote-tracking branch 'upstream/master'

new update merge
pull/816/head
Shubham Verma 2020-09-28 09:30:05 +00:00
commit 44ceca711c
52 changed files with 710 additions and 500 deletions

2
.gitignore vendored
View File

@ -16,8 +16,8 @@
# production # production
/build /build
/public/umami.js /public/umami.js
/public/geo
/lang-compiled /lang-compiled
/lang-formatted
# misc # misc
.DS_Store .DS_Store

View File

@ -5,18 +5,20 @@ ENV DATABASE_URL "postgresql://umami:umami@db:5432/umami" \
DATABASE_TYPE=$DATABASE_TYPE DATABASE_TYPE=$DATABASE_TYPE
WORKDIR /build WORKDIR /build
RUN yarn config set --home enableTelemetry 0
COPY package.json yarn.lock /build/ COPY package.json yarn.lock /build/
# Install only the production dependencies # Install only the production dependencies
RUN yarn install --production RUN yarn install --production --frozen-lockfile
# Cache these modules for production # Cache these modules for production
RUN cp -R node_modules/ prod_node_modules/ RUN cp -R node_modules/ prod_node_modules/
# Install development dependencies # Install development dependencies
RUN yarn install RUN yarn install --frozen-lockfile
COPY . /build COPY . /build
RUN yarn next telemetry disable
RUN yarn build RUN yarn build
# Production image # Production image
@ -27,7 +29,7 @@ WORKDIR /app
COPY --from=build /build/prod_node_modules ./node_modules COPY --from=build /build/prod_node_modules ./node_modules
# Copy generated Prisma client # Copy generated Prisma client
COPY --from=build /build/node_modules/\.prisma/ ./node_modules/\.prisma/ COPY --from=build /build/node_modules/.prisma/ ./node_modules/.prisma/
COPY --from=build /build/yarn.lock /build/package.json ./ COPY --from=build /build/yarn.lock /build/package.json ./
COPY --from=build /build/.next ./.next COPY --from=build /build/.next ./.next

View File

@ -1,12 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import classNames from 'classnames'; import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart'; import WebsiteChart from 'components/metrics/WebsiteChart';
import WorldMap from 'components/common/WorldMap'; import WorldMap from 'components/common/WorldMap';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import MenuLayout from 'components/layout/MenuLayout'; import MenuLayout from 'components/layout/MenuLayout';
import Button from 'components/common/Button'; import Link from 'components/common/Link';
import Loading from 'components/common/Loading';
import Arrow from 'assets/arrow-right.svg'; import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteDetails.module.css'; import styles from './WebsiteDetails.module.css';
import PagesTable from './metrics/PagesTable'; import PagesTable from './metrics/PagesTable';
@ -18,7 +18,7 @@ import CountriesTable from './metrics/CountriesTable';
import EventsTable from './metrics/EventsTable'; import EventsTable from './metrics/EventsTable';
import EventsChart from './metrics/EventsChart'; import EventsChart from './metrics/EventsChart';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import Loading from 'components/common/Loading'; import usePageQuery from 'hooks/usePageQuery';
const views = { const views = {
url: PagesTable, url: PagesTable,
@ -31,31 +31,27 @@ const views = {
}; };
export default function WebsiteDetails({ websiteId, token }) { export default function WebsiteDetails({ websiteId, token }) {
const router = useRouter();
const { data } = useFetch(`/api/website/${websiteId}`, { token }); const { data } = useFetch(`/api/website/${websiteId}`, { token });
const [chartLoaded, setChartLoaded] = useState(false); const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState(); const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState(); const [eventsData, setEventsData] = useState();
const { const {
query: { id, view }, resolve,
basePath, router,
asPath, query: { view },
} = router; } = usePageQuery();
const path = `${basePath}/${asPath.split('/')[1]}/${id.join('/')}`;
const BackButton = () => ( const BackButton = () => (
<Button <Link
key="back-button" key="back-button"
className={styles.backButton} className={styles.backButton}
href={router.pathname}
as={resolve({ view: undefined })}
icon={<Arrow />} icon={<Arrow />}
size="xsmall" size="small"
onClick={() => router.push(path)}
> >
<div> <FormattedMessage id="button.back" defaultMessage="Back" />
<FormattedMessage id="button.back" defaultMessage="Back" /> </Link>
</div>
</Button>
); );
const menuOptions = [ const menuOptions = [
@ -64,31 +60,31 @@ export default function WebsiteDetails({ websiteId, token }) {
}, },
{ {
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />, label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
value: `${path}?view=url`, value: resolve({ view: 'url' }),
}, },
{ {
label: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />, label: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />,
value: `${path}?view=referrer`, value: resolve({ view: 'referrer' }),
}, },
{ {
label: <FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />, label: <FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />,
value: `${path}?view=browser`, value: resolve({ view: 'browser' }),
}, },
{ {
label: <FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />, label: <FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />,
value: `${path}?view=os`, value: resolve({ view: 'os' }),
}, },
{ {
label: <FormattedMessage id="metrics.devices" defaultMessage="Devices" />, label: <FormattedMessage id="metrics.devices" defaultMessage="Devices" />,
value: `${path}?view=device`, value: resolve({ view: 'device' }),
}, },
{ {
label: <FormattedMessage id="metrics.countries" defaultMessage="Countries" />, label: <FormattedMessage id="metrics.countries" defaultMessage="Countries" />,
value: `${path}?view=country`, value: resolve({ view: 'country' }),
}, },
{ {
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />, label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
value: `${path}?view=event`, value: resolve({ view: 'event' }),
}, },
]; ];
@ -97,7 +93,6 @@ export default function WebsiteDetails({ websiteId, token }) {
token, token,
websiteDomain: data?.domain, websiteDomain: data?.domain,
limit: 10, limit: 10,
onExpand: handleExpand,
}; };
const DetailsComponent = views[view]; const DetailsComponent = views[view];
@ -108,10 +103,6 @@ export default function WebsiteDetails({ websiteId, token }) {
} }
} }
function handleExpand(value) {
router.push(`${path}?view=${value}`);
}
if (!data) { if (!data) {
return null; return null;
} }
@ -179,7 +170,7 @@ export default function WebsiteDetails({ websiteId, token }) {
contentClassName={styles.content} contentClassName={styles.content}
menu={menuOptions} menu={menuOptions}
> >
<DetailsComponent {...tableProps} limit={false} /> <DetailsComponent {...tableProps} limit={false} showFilters={true} />
</MenuLayout> </MenuLayout>
)} )}
</Page> </Page>

View File

@ -16,7 +16,6 @@
} }
.backButton { .backButton {
align-self: flex-start;
margin-bottom: 16px; margin-bottom: 16px;
} }

View File

@ -34,9 +34,7 @@ export default function WebsiteList({ userId }) {
} }
> >
<Button icon={<Arrow />} size="medium" onClick={() => router.push('/settings')}> <Button icon={<Arrow />} size="medium" onClick={() => router.push('/settings')}>
<div> <FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
</div>
</Button> </Button>
</EmptyPlaceholder> </EmptyPlaceholder>
)} )}

View File

@ -13,7 +13,8 @@ export default function Button({
className, className,
tooltip, tooltip,
tooltipId, tooltipId,
disabled = false, disabled,
iconRight,
onClick = () => {}, onClick = () => {},
...props ...props
}) { }) {
@ -30,14 +31,14 @@ export default function Button({
[styles.action]: variant === 'action', [styles.action]: variant === 'action',
[styles.danger]: variant === 'danger', [styles.danger]: variant === 'danger',
[styles.light]: variant === 'light', [styles.light]: variant === 'light',
[styles.disabled]: disabled, [styles.iconRight]: iconRight,
})} })}
disabled={disabled} disabled={disabled}
onClick={!disabled ? onClick : null} onClick={!disabled ? onClick : null}
{...props} {...props}
> >
{icon && <Icon icon={icon} size={size} />} {icon && <Icon className={styles.icon} icon={icon} size={size} />}
{children} {children && <div className={styles.label}>{children}</div>}
{tooltip && <ReactTooltip id={tooltipId}>{tooltip}</ReactTooltip>} {tooltip && <ReactTooltip id={tooltipId}>{tooltip}</ReactTooltip>}
</button> </button>
); );

View File

@ -10,7 +10,6 @@
border: 0; border: 0;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
white-space: nowrap;
position: relative; position: relative;
} }
@ -22,12 +21,15 @@
color: var(--gray900); color: var(--gray900);
} }
.large { .label {
font-size: var(--font-size-large); white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 300px;
} }
.medium { .large {
font-size: var(--font-size-normal); font-size: var(--font-size-large);
} }
.small { .small {
@ -38,7 +40,8 @@
font-size: var(--font-size-xsmall); font-size: var(--font-size-xsmall);
} }
.action { .action,
.action:active {
color: var(--gray50); color: var(--gray50);
background: var(--gray900); background: var(--gray900);
} }
@ -47,7 +50,8 @@
background: var(--gray800); background: var(--gray800);
} }
.danger { .danger,
.danger:active {
color: var(--gray50); color: var(--gray50);
background: var(--red500); background: var(--red500);
} }
@ -56,7 +60,9 @@
background: var(--red400); background: var(--red400);
} }
.light { .light,
.light:active {
color: var(--gray900);
background: transparent; background: transparent;
} }
@ -64,6 +70,19 @@
background: inherit; background: inherit;
} }
.button .icon + * {
margin-left: 10px;
}
.button.iconRight .icon {
order: 1;
margin-left: 10px;
}
.button.iconRight .icon + * {
margin: 0;
}
.button:disabled { .button:disabled {
cursor: default; cursor: default;
color: var(--gray500); color: var(--gray500);

View File

@ -5,10 +5,6 @@
vertical-align: middle; vertical-align: middle;
} }
.icon + * {
margin-left: 10px;
}
.icon svg { .icon svg {
fill: currentColor; fill: currentColor;
} }

View File

@ -1,12 +1,23 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import NextLink from 'next/link'; import NextLink from 'next/link';
import Icon from './Icon';
import styles from './Link.module.css'; import styles from './Link.module.css';
export default function Link({ className, children, ...props }) { export default function Link({ className, icon, children, size, iconRight, ...props }) {
return ( return (
<NextLink {...props}> <NextLink {...props}>
<a className={classNames(styles.link, className)}>{children}</a> <a
className={classNames(styles.link, className, {
[styles.large]: size === 'large',
[styles.small]: size === 'small',
[styles.xsmall]: size === 'xsmall',
[styles.iconRight]: iconRight,
})}
>
{icon && <Icon className={styles.icon} icon={icon} size={size} />}
{children}
</a>
</NextLink> </NextLink>
); );
} }

View File

@ -4,6 +4,8 @@ a.link:visited {
position: relative; position: relative;
color: var(--gray900); color: var(--gray900);
text-decoration: none; text-decoration: none;
display: inline-flex;
align-items: center;
} }
a.link:before { a.link:before {
@ -21,3 +23,28 @@ a.link:hover:before {
width: 100%; width: 100%;
transition: width 100ms; transition: width 100ms;
} }
a.link.large {
font-size: var(--font-size-large);
}
a.link.small {
font-size: var(--font-size-small);
}
a.link.xsmall {
font-size: var(--font-size-xsmall);
}
a.link .icon + * {
margin-left: 10px;
}
a.link.iconRight .icon {
order: 1;
margin-left: 10px;
}
a.link.iconRight .icon + * {
margin: 0;
}

View File

@ -5,6 +5,9 @@
.title { .title {
font-size: var(--font-size-large); font-size: var(--font-size-large);
display: flex;
align-items: center;
line-height: 1.4;
} }
.logo { .logo {
@ -26,11 +29,12 @@
.buttons { .buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center;
} }
@media only screen and (max-width: 992px) { @media only screen and (max-width: 992px) {
.title { .title {
text-align: center; justify-content: center;
} }
.nav { .nav {

View File

@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { browserFilter } from 'lib/filters'; import { browserFilter } from 'lib/filters';
export default function BrowsersTable({ websiteId, token, limit, onExpand }) { export default function BrowsersTable({ websiteId, token, limit }) {
return ( return (
<MetricsTable <MetricsTable
title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />} title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />}
@ -13,7 +13,6 @@ export default function BrowsersTable({ websiteId, token, limit, onExpand }) {
token={token} token={token}
limit={limit} limit={limit}
dataFilter={browserFilter} dataFilter={browserFilter}
onExpand={onExpand}
/> />
); );
} }

View File

@ -3,13 +3,7 @@ import MetricsTable from './MetricsTable';
import { countryFilter, percentFilter } from 'lib/filters'; import { countryFilter, percentFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export default function CountriesTable({ export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) {
websiteId,
token,
limit,
onDataLoad = () => {},
onExpand,
}) {
return ( return (
<MetricsTable <MetricsTable
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />} title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
@ -20,7 +14,6 @@ export default function CountriesTable({
limit={limit} limit={limit}
dataFilter={countryFilter} dataFilter={countryFilter}
onDataLoad={data => onDataLoad(percentFilter(data))} onDataLoad={data => onDataLoad(percentFilter(data))}
onExpand={onExpand}
/> />
); );
} }

View File

@ -4,7 +4,7 @@ import { deviceFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { getDeviceMessage } from 'components/messages'; import { getDeviceMessage } from 'components/messages';
export default function DevicesTable({ websiteId, token, limit, onExpand }) { export default function DevicesTable({ websiteId, token, limit }) {
return ( return (
<MetricsTable <MetricsTable
title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />} title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />}
@ -15,7 +15,6 @@ export default function DevicesTable({ websiteId, token, limit, onExpand }) {
limit={limit} limit={limit}
dataFilter={deviceFilter} dataFilter={deviceFilter}
renderLabel={({ x }) => getDeviceMessage(x)} renderLabel={({ x }) => getDeviceMessage(x)}
onExpand={onExpand}
/> />
); );
} }

View File

@ -6,11 +6,13 @@ import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone'; import useTimezone from 'hooks/useTimezone';
import { EVENT_COLORS } from 'lib/constants'; import { EVENT_COLORS } from 'lib/constants';
import usePageQuery from '../../hooks/usePageQuery';
export default function EventsChart({ websiteId, token }) { export default function EventsChart({ websiteId, token }) {
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, modified } = dateRange; const { startDate, endDate, unit, modified } = dateRange;
const [timezone] = useTimezone(); const [timezone] = useTimezone();
const { query } = usePageQuery();
const { data } = useFetch( const { data } = useFetch(
`/api/website/${websiteId}/events`, `/api/website/${websiteId}/events`,
@ -19,6 +21,7 @@ export default function EventsChart({ websiteId, token }) {
end_at: +endDate, end_at: +endDate,
unit, unit,
tz: timezone, tz: timezone,
url: query.url,
token, token,
}, },
{ update: [modified] }, { update: [modified] },

View File

@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css'; import styles from './EventsTable.module.css';
export default function EventsTable({ websiteId, token, limit, onExpand, onDataLoad }) { export default function EventsTable({ websiteId, token, limit, onDataLoad }) {
return ( return (
<MetricsTable <MetricsTable
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />} title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
@ -13,7 +13,6 @@ export default function EventsTable({ websiteId, token, limit, onExpand, onDataL
token={token} token={token}
limit={limit} limit={limit}
renderLabel={({ x }) => <Label value={x} />} renderLabel={({ x }) => <Label value={x} />}
onExpand={onExpand}
onDataLoad={onDataLoad} onDataLoad={onDataLoad}
/> />
); );

View File

@ -5,24 +5,30 @@ import Loading from 'components/common/Loading';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format'; import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import usePageQuery from 'hooks/usePageQuery';
import MetricCard from './MetricCard'; import MetricCard from './MetricCard';
import styles from './MetricsBar.module.css'; import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId, token, className }) { export default function MetricsBar({ websiteId, token, className }) {
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange; const { startDate, endDate, modified } = dateRange;
const [format, setFormat] = useState(true);
const {
query: { url },
} = usePageQuery();
const { data } = useFetch( const { data } = useFetch(
`/api/website/${websiteId}/metrics`, `/api/website/${websiteId}/metrics`,
{ {
start_at: +startDate, start_at: +startDate,
end_at: +endDate, end_at: +endDate,
url,
token, token,
}, },
{ {
update: [modified], update: [modified],
}, },
); );
const [format, setFormat] = useState(true);
const formatFunc = format ? formatLongNumber : formatNumber; const formatFunc = format ? formatLongNumber : formatNumber;

View File

@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring'; import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames'; import classNames from 'classnames';
import Button from 'components/common/Button'; import Link from 'components/common/Link';
import Loading from 'components/common/Loading'; import Loading from 'components/common/Loading';
import NoData from 'components/common/NoData'; import NoData from 'components/common/NoData';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
@ -11,6 +11,7 @@ import Arrow from 'assets/arrow-right.svg';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format'; import { formatNumber, formatLongNumber } from 'lib/format';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery';
import styles from './MetricsTable.module.css'; import styles from './MetricsTable.module.css';
export default function MetricsTable({ export default function MetricsTable({
@ -26,10 +27,15 @@ export default function MetricsTable({
limit, limit,
renderLabel, renderLabel,
onDataLoad = () => {}, onDataLoad = () => {},
onExpand = () => {},
}) { }) {
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange; const { startDate, endDate, modified } = dateRange;
const {
resolve,
router,
query: { url },
} = usePageQuery();
const { data } = useFetch( const { data } = useFetch(
`/api/website/${websiteId}/rankings`, `/api/website/${websiteId}/rankings`,
{ {
@ -37,6 +43,7 @@ export default function MetricsTable({
start_at: +startDate, start_at: +startDate,
end_at: +endDate, end_at: +endDate,
domain: websiteDomain, domain: websiteDomain,
url,
token, token,
}, },
{ onDataLoad, delay: 300, update: [modified] }, { onDataLoad, delay: 300, update: [modified] },
@ -99,12 +106,16 @@ export default function MetricsTable({
)} )}
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>
{limit && data.length > limit && ( {limit && (
<Button icon={<Arrow />} size="xsmall" onClick={() => onExpand(type)}> <Link
<div> icon={<Arrow />}
<FormattedMessage id="button.more" defaultMessage="More" /> href={router.pathname}
</div> as={resolve({ view: type })}
</Button> size="small"
iconRight
>
<FormattedMessage id="button.more" defaultMessage="More" />
</Link>
)} )}
</div> </div>
</> </>

View File

@ -1,6 +1,6 @@
.container { .container {
position: relative; position: relative;
min-height: 460px; min-height: 430px;
font-size: var(--font-size-small); font-size: var(--font-size-small);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -21,6 +21,7 @@
.metric { .metric {
font-size: var(--font-size-small); font-size: var(--font-size-small);
font-weight: 600;
text-align: center; text-align: center;
width: 100px; width: 100px;
cursor: pointer; cursor: pointer;
@ -72,8 +73,8 @@
.percent { .percent {
position: relative; position: relative;
width: 50px; width: 50px;
color: #6e6e6e; color: var(--gray600);
border-left: 1px solid var(--gray500); border-left: 1px solid var(--gray600);
padding-left: 10px; padding-left: 10px;
z-index: 1; z-index: 1;
} }

View File

@ -3,7 +3,7 @@ import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters'; import { osFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export default function OSTable({ websiteId, token, limit, onExpand }) { export default function OSTable({ websiteId, token, limit }) {
return ( return (
<MetricsTable <MetricsTable
title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />} title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />}
@ -13,7 +13,6 @@ export default function OSTable({ websiteId, token, limit, onExpand }) {
token={token} token={token}
limit={limit} limit={limit}
dataFilter={osFilter} dataFilter={osFilter}
onExpand={onExpand}
/> />
); );
} }

View File

@ -1,13 +1,21 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Link from 'next/link';
import ButtonGroup from 'components/common/ButtonGroup'; import ButtonGroup from 'components/common/ButtonGroup';
import ButtonLayout from 'components/layout/ButtonLayout';
import { urlFilter } from 'lib/filters'; import { urlFilter } from 'lib/filters';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants'; import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import usePageQuery from 'hooks/usePageQuery';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import ButtonLayout from '../layout/ButtonLayout'; import styles from './PagesTable.module.css';
export default function PagesTable({ websiteId, token, websiteDomain, limit, onExpand }) { export default function PagesTable({ websiteId, token, websiteDomain, limit, showFilters }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const {
resolve,
query: { url },
} = usePageQuery();
const buttons = [ const buttons = [
{ {
@ -17,9 +25,24 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, onE
{ label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW }, { label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
]; ];
const renderLink = ({ x }) => {
return (
<Link href={resolve({ url: x })} replace={true}>
<a
className={classNames({
[styles.inactive]: url && x !== url,
[styles.active]: x === url,
})}
>
{decodeURI(x)}
</a>
</Link>
);
};
return ( return (
<> <>
{!limit && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />} {showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
<MetricsTable <MetricsTable
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />} title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
type="url" type="url"
@ -29,8 +52,7 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, onE
limit={limit} limit={limit}
dataFilter={urlFilter} dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }} filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}
renderLabel={({ x }) => decodeURI(x)} renderLabel={renderLink}
onExpand={onExpand}
/> />
</> </>
); );

View File

@ -0,0 +1,8 @@
body .inactive {
color: var(--gray500);
}
body .active {
color: var(--gray900);
font-weight: 600;
}

View File

@ -6,13 +6,7 @@ import ButtonGroup from 'components/common/ButtonGroup';
import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants'; import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import ButtonLayout from '../layout/ButtonLayout'; import ButtonLayout from '../layout/ButtonLayout';
export default function ReferrersTable({ export default function ReferrersTable({ websiteId, websiteDomain, token, limit, showFilters }) {
websiteId,
websiteDomain,
token,
limit,
onExpand = () => {},
}) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [ const buttons = [
@ -39,7 +33,7 @@ export default function ReferrersTable({
return ( return (
<> <>
{!limit && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />} {showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
<MetricsTable <MetricsTable
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />} title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
type="referrer" type="referrer"
@ -54,7 +48,6 @@ export default function ReferrersTable({
domainOnly: filter === FILTER_DOMAIN_ONLY, domainOnly: filter === FILTER_DOMAIN_ONLY,
raw: filter === FILTER_RAW, raw: filter === FILTER_RAW,
}} }}
onExpand={onExpand}
renderLabel={renderLink} renderLabel={renderLink}
/> />
</> </>

View File

@ -5,10 +5,13 @@ import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader'; import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/common/DateFilter'; import DateFilter from 'components/common/DateFilter';
import StickyHeader from 'components/helpers/StickyHeader'; import StickyHeader from 'components/helpers/StickyHeader';
import Button from 'components/common/Button';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone'; import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength } from 'lib/date'; import { getDateArray, getDateLength } from 'lib/date';
import Times from 'assets/times.svg';
import styles from './WebsiteChart.module.css'; import styles from './WebsiteChart.module.css';
export default function WebsiteChart({ export default function WebsiteChart({
@ -22,6 +25,11 @@ export default function WebsiteChart({
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange; const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone(); const [timezone] = useTimezone();
const {
router,
resolve,
query: { url },
} = usePageQuery();
const { data, loading } = useFetch( const { data, loading } = useFetch(
`/api/website/${websiteId}/pageviews`, `/api/website/${websiteId}/pageviews`,
@ -30,6 +38,7 @@ export default function WebsiteChart({
end_at: +endDate, end_at: +endDate,
unit, unit,
tz: timezone, tz: timezone,
url,
token, token,
}, },
{ onDataLoad, update: [modified] }, { onDataLoad, update: [modified] },
@ -45,6 +54,10 @@ export default function WebsiteChart({
return [[], []]; return [[], []];
}, [data]); }, [data]);
function handleCloseFilter() {
router.push(resolve({ url: undefined }));
}
return ( return (
<> <>
<WebsiteHeader websiteId={websiteId} token={token} title={title} showLink={showLink} /> <WebsiteHeader websiteId={websiteId} token={token} title={title} showLink={showLink} />
@ -54,6 +67,7 @@ export default function WebsiteChart({
stickyClassName={styles.sticky} stickyClassName={styles.sticky}
enabled={stickyHeader} enabled={stickyHeader}
> >
{url && <PageFilter url={url} onClick={handleCloseFilter} />}
<div className="col-12 col-lg-9"> <div className="col-12 col-lg-9">
<MetricsBar websiteId={websiteId} token={token} /> <MetricsBar websiteId={websiteId} token={token} />
</div> </div>
@ -81,3 +95,13 @@ export default function WebsiteChart({
</> </>
); );
} }
const PageFilter = ({ url, onClick }) => {
return (
<div className={classNames(styles.url, 'col-12')}>
<Button icon={<Times />} onClick={onClick} variant="action" iconRight>
{url}
</Button>
</div>
);
};

View File

@ -36,6 +36,11 @@
align-items: center; align-items: center;
} }
.url {
text-align: center;
margin-bottom: 10px;
}
@media only screen and (max-width: 992px) { @media only screen and (max-width: 992px) {
.filter { .filter {
display: block; display: block;

View File

@ -4,7 +4,6 @@ import Link from 'components/common/Link';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import RefreshButton from 'components/common/RefreshButton'; import RefreshButton from 'components/common/RefreshButton';
import ButtonLayout from 'components/layout/ButtonLayout'; import ButtonLayout from 'components/layout/ButtonLayout';
import Icon from 'components/common/Icon';
import ActiveUsers from './ActiveUsers'; import ActiveUsers from './ActiveUsers';
import Arrow from 'assets/arrow-right.svg'; import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteHeader.module.css'; import styles from './WebsiteHeader.module.css';
@ -21,9 +20,11 @@ export default function WebsiteHeader({ websiteId, token, title, showLink = fals
href="/website/[...id]" href="/website/[...id]"
as={`/website/${websiteId}/${title}`} as={`/website/${websiteId}/${title}`}
className={styles.link} className={styles.link}
icon={<Arrow />}
size="small"
iconRight
> >
<FormattedMessage id="button.view-details" defaultMessage="View details" /> <FormattedMessage id="button.view-details" defaultMessage="View details" />
<Icon icon={<Arrow />} size="small" />
</Link> </Link>
)} )}
</ButtonLayout> </ButtonLayout>

View File

@ -5,14 +5,9 @@
} }
.link { .link {
font-size: var(--font-size-small);
font-weight: 600; font-weight: 600;
} }
.link svg {
margin-left: 10px;
}
@media only screen and (max-width: 576px) { @media only screen and (max-width: 576px) {
.active { .active {
display: none; display: none;

View File

@ -42,14 +42,10 @@ export default function AccountSettings() {
row.username !== 'admin' ? ( row.username !== 'admin' ? (
<ButtonLayout align="right"> <ButtonLayout align="right">
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}> <Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
<div> <FormattedMessage id="button.edit" defaultMessage="Edit" />
<FormattedMessage id="button.edit" defaultMessage="Edit" />
</div>
</Button> </Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}> <Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
<div> <FormattedMessage id="button.delete" defaultMessage="Delete" />
<FormattedMessage id="button.delete" defaultMessage="Delete" />
</div>
</Button> </Button>
</ButtonLayout> </ButtonLayout>
) : null; ) : null;
@ -102,9 +98,7 @@ export default function AccountSettings() {
<FormattedMessage id="label.accounts" defaultMessage="Accounts" /> <FormattedMessage id="label.accounts" defaultMessage="Accounts" />
</div> </div>
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}> <Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}>
<div> <FormattedMessage id="button.add-account" defaultMessage="Add account" />
<FormattedMessage id="button.add-account" defaultMessage="Add account" />
</div>
</Button> </Button>
</PageHeader> </PageHeader>
<Table columns={columns} rows={data} /> <Table columns={columns} rows={data} />

View File

@ -29,9 +29,7 @@ export default function ProfileSettings() {
<FormattedMessage id="label.profile" defaultMessage="Profile" /> <FormattedMessage id="label.profile" defaultMessage="Profile" />
</div> </div>
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}> <Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}>
<div> <FormattedMessage id="button.change-password" defaultMessage="Change password" />
<FormattedMessage id="button.change-password" defaultMessage="Change password" />
</div>
</Button> </Button>
</PageHeader> </PageHeader>
<dl className={styles.list}> <dl className={styles.list}>

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { useTransition, animated } from 'react-spring'; import { useTransition, animated } from 'react-spring';
import Button from 'components/common/Button';
import useTheme from 'hooks/useTheme'; import useTheme from 'hooks/useTheme';
import Sun from 'assets/sun.svg'; import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg'; import Moon from 'assets/moon.svg';
@ -27,7 +26,7 @@ export default function ThemeButton() {
} }
return ( return (
<Button className={styles.button} variant="light" onClick={handleClick}> <div className={styles.button} onClick={handleClick}>
{transitions.map(({ item, key, props }) => {transitions.map(({ item, key, props }) =>
item === 'light' ? ( item === 'light' ? (
<animated.div key={key} style={props}> <animated.div key={key} style={props}>
@ -39,6 +38,6 @@ export default function ThemeButton() {
</animated.div> </animated.div>
), ),
)} )}
</Button> </div>
); );
} }

View File

@ -1,5 +1,10 @@
.button { .button {
width: 50px; width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
} }
.button svg { .button svg {

View File

@ -52,14 +52,10 @@ export default function WebsiteSettings() {
onClick={() => setShowCode(row)} onClick={() => setShowCode(row)}
/> />
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}> <Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
<div> <FormattedMessage id="button.edit" defaultMessage="Edit" />
<FormattedMessage id="button.edit" defaultMessage="Edit" />
</div>
</Button> </Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}> <Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
<div> <FormattedMessage id="button.delete" defaultMessage="Delete" />
<FormattedMessage id="button.delete" defaultMessage="Delete" />
</div>
</Button> </Button>
</ButtonLayout> </ButtonLayout>
); );
@ -117,9 +113,7 @@ export default function WebsiteSettings() {
} }
> >
<Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}> <Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}>
<div> <FormattedMessage id="button.add-website" defaultMessage="Add website" />
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
</div>
</Button> </Button>
</EmptyPlaceholder> </EmptyPlaceholder>
); );
@ -131,9 +125,7 @@ export default function WebsiteSettings() {
<FormattedMessage id="label.websites" defaultMessage="Websites" /> <FormattedMessage id="label.websites" defaultMessage="Websites" />
</div> </div>
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}> <Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}>
<div> <FormattedMessage id="button.add-website" defaultMessage="Add website" />
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
</div>
</Button> </Button>
</PageHeader> </PageHeader>
<Table columns={columns} rows={data} empty={empty} /> <Table columns={columns} rows={data} empty={empty} />

View File

@ -12,7 +12,7 @@ services:
depends_on: depends_on:
- db - db
db: db:
image: postgres:alpine image: postgres:12-alpine
environment: environment:
POSTGRES_DB: umami POSTGRES_DB: umami
POSTGRES_USER: umami POSTGRES_USER: umami

32
hooks/usePageQuery.js Normal file
View File

@ -0,0 +1,32 @@
import { useMemo } from 'react';
import { useRouter } from 'next/router';
import { getQueryString } from '../lib/url';
export default function usePageQuery() {
const router = useRouter();
const { pathname, search } = location;
const query = useMemo(() => {
if (!search) {
return {};
}
const params = search.substring(1).split('&');
return params.reduce((obj, item) => {
const [key, value] = item.split('=');
obj[key] = decodeURIComponent(value);
return obj;
}, {});
}, [search]);
function resolve(params) {
const search = getQueryString({ ...query, ...params });
return `${pathname}${search}`;
}
return { pathname, query, resolve, router };
}

95
lang/fo-FO.json Normal file
View File

@ -0,0 +1,95 @@
{
"button.add-account": "Ger brúkara",
"button.add-website": "Legg heimasíðu til",
"button.back": "Aftur",
"button.cancel": "Strika",
"button.change-password": "Broyt loyniorð",
"button.copy-to-clipboard": "Kopier til clipboard",
"button.date-range": "Vel dato",
"button.delete": "Sletta",
"button.edit": "Ger broyting",
"button.login": "Rita inn",
"button.more": "Meira",
"button.refresh": "Endurskapa",
"button.reset": "Nulstilla",
"button.save": "Goym",
"button.single-day": "Einkultur dagur",
"button.view-details": "Vís upplýsingar",
"label.accounts": "Brúkarar",
"label.administrator": "Administrator",
"label.confirm-password": "Vátta loyniorð",
"label.current-password": "Núverandi loyniorð",
"label.custom-range": "Tillaga spenni",
"label.dashboard": "Yvirlitsskíggi",
"label.default-date-range": "Standard dato",
"label.domain": "Økisnavn",
"label.enable-share-url": "Virkja deili leinki",
"label.invalid": "Ógilda",
"label.invalid-domain": "Ógilt økisnavn",
"label.last-days": "Seinastu {x} dagarnar",
"label.last-hours": "Seinastu {x} tímanar",
"label.logged-in-as": "Ritaður inn sum {username}",
"label.logout": "Rita út",
"label.name": "Navn",
"label.new-password": "Nýtt loyniorð",
"label.password": "Loyniorð",
"label.passwords-dont-match": "Loyniorðini eru ikki eins",
"label.profile": "Brúkari",
"label.required": "Krav",
"label.settings": "Stillingar",
"label.this-month": "Hendan mánan",
"label.this-week": "Hesa vikuna",
"label.this-year": "Hetta árið",
"label.timezone": "Tíðarsona",
"label.today": "Í dag",
"label.unknown": "Ókent",
"label.username": "Brúkaranavn",
"label.websites": "Heimasíður",
"message.active-users": "{x} í løtuni {x, plural, one {vitjandi} other { vitjandi }}",
"message.confirm-delete": "Ert tú sikkur at tú ynskir at sletta {target}?",
"message.copied": "Kopiera!",
"message.delete-warning": "Øll data ið er knýtt at verður eisini sletta.",
"message.failure": "Okkurt bleiv gali.",
"message.get-share-url": "Fá leinku sum tú kanst deila",
"message.get-tracking-code": "Fá sporings kotu",
"message.go-to-settings": "Far til stillingar",
"message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.",
"message.no-data-available": "Einki data tøk.",
"message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.",
"message.page-not-found": "Síðan bleiv ikki funnin.",
"message.powered-by": "Powered by {name}",
"message.save-success": "Goymt.",
"message.share-url": "Hetta er tann almenna leinkan av {target}.",
"message.track-stats": "Fyri at spora hagtøl fyri {target}, koyr kotuna í {head} partin á tínari heimasíðu.",
"message.type-delete": "Skriva {delete} í feltið fyri at vátta",
"metrics.actions": "Gerðir",
"metrics.average-visit-time": "Miðal vitjurnartíð ",
"metrics.bounce-rate": "Bounce prosenttal",
"metrics.browsers": "Kagar",
"metrics.countries": "Lond",
"metrics.device.desktop": "Borðtelda",
"metrics.device.laptop": "Fartelda",
"metrics.device.mobile": "Telefon",
"metrics.device.tablet": "Teldil",
"metrics.devices": "Tóleindir",
"metrics.events": "Hendingar/tiltøk",
"metrics.filter.combined": "Samansett",
"metrics.filter.domain-only": "Bara økisnavn",
"metrics.filter.raw": "Óviðgjørt",
"metrics.operating-systems": "Stýrikervir",
"metrics.page-views": "Opnaðar síðir",
"metrics.pages": "Síðir",
"metrics.referrers": "Framsendingar",
"metrics.unique-visitors": "Einsýna vitjanir",
"metrics.views": "Vitjanir",
"metrics.visitors": "Vitjandi",
"title.add-account": "Ger brúkara",
"title.add-website": "Legg heimasíðu avtrat",
"title.change-password": "Skift loyniorð",
"title.delete-account": "Sletta brúkara",
"title.delete-website": "Sletta heimasíðu",
"title.edit-account": "Broyt brúkara",
"title.edit-website": "Broyt heimasíðu",
"title.share-url": "Deil leinku",
"title.tracking-code": "Spori kota"
}

View File

@ -75,10 +75,10 @@
"metrics.events": "Händelser", "metrics.events": "Händelser",
"metrics.filter.combined": "Kombinerade", "metrics.filter.combined": "Kombinerade",
"metrics.filter.domain-only": "Endast domän", "metrics.filter.domain-only": "Endast domän",
"metrics.filter.raw": "Rå", "metrics.filter.raw": "Rådata",
"metrics.operating-systems": "Operativsystem", "metrics.operating-systems": "Operativsystem",
"metrics.page-views": "Sidvisningar", "metrics.page-views": "Sidvisningar",
"metrics.pages": "Sido", "metrics.pages": "Sidor",
"metrics.referrers": "Hänvisare", "metrics.referrers": "Hänvisare",
"metrics.unique-visitors": "Unika besökare", "metrics.unique-visitors": "Unika besökare",
"metrics.views": "Visningar", "metrics.views": "Visningar",

View File

@ -100,6 +100,19 @@ export function getDateRangeValues(startDate, endDate) {
return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit }; return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit };
} }
export function getDateFromString(str) {
const [ymd, hms] = str.split(' ');
const [year, month, day] = ymd.split('-');
if (hms) {
const [hour, min, sec] = hms.split(':');
return new Date(year, month - 1, day, hour, min, sec);
}
return new Date(year, month - 1, day);
}
const dateFuncs = { const dateFuncs = {
hour: [differenceInHours, addHours, startOfHour], hour: [differenceInHours, addHours, startOfHour],
day: [differenceInCalendarDays, addDays, startOfDay], day: [differenceInCalendarDays, addDays, startOfDay],
@ -114,12 +127,7 @@ export function getDateArray(data, startDate, endDate, unit) {
function findData(t) { function findData(t) {
const x = data.find(e => { const x = data.find(e => {
if (unit === 'hour') { return normalize(getDateFromString(e.t)).getTime() === t.getTime();
return normalize(new Date(e.t)).getTime() === t.getTime();
}
const [year, month, day] = e.t.split('-');
return normalize(new Date(year, month - 1, day)).getTime() === t.getTime();
}); });
return x?.y || 0; return x?.y || 0;

View File

@ -31,9 +31,3 @@ if (process.env.NODE_ENV === 'production') {
} }
export default prisma; export default prisma;
export async function runQuery(query) {
return query.catch(e => {
throw e;
});
}

View File

@ -13,17 +13,13 @@ export const urlFilter = (data, { raw }) => {
const cleanUrl = url => { const cleanUrl = url => {
try { try {
const { pathname, search, searchParams } = new URL(url); const { pathname, search } = new URL(url, location.origin);
if (search.startsWith('?/')) { if (search.startsWith('?/')) {
return `${pathname}${search}`; return `${pathname}${search}`;
} }
const path = removeTrailingSlash(pathname); return removeTrailingSlash(pathname);
const ref = searchParams.get('ref');
const query = ref ? `?ref=${ref}` : '';
return `${path}${query}`;
} catch { } catch {
return null; return null;
} }
@ -34,7 +30,7 @@ export const urlFilter = (data, { raw }) => {
return obj; return obj;
} }
const url = cleanUrl(`http://x${x}`); const url = cleanUrl(x);
if (url) { if (url) {
if (!obj[url]) { if (!obj[url]) {

View File

@ -13,6 +13,7 @@ import mnMNMessages from 'lang-compiled/mn-MN.json';
import daMessages from 'lang-compiled/da-DK.json'; import daMessages from 'lang-compiled/da-DK.json';
import svMessages from 'lang-compiled/sv-SE.json'; import svMessages from 'lang-compiled/sv-SE.json';
import grMessages from 'lang-compiled/el-GR.json'; import grMessages from 'lang-compiled/el-GR.json';
import foMessages from 'lang-compiled/fo-FO.json';
export const messages = { export const messages = {
'en-US': enMessages, 'en-US': enMessages,
@ -28,6 +29,7 @@ export const messages = {
'da-DK': daMessages, 'da-DK': daMessages,
'sv-SE': svMessages, 'sv-SE': svMessages,
'el-GR': grMessages, 'el-GR': grMessages,
'fo-FO': foMessages,
}; };
export const dateLocales = { export const dateLocales = {
@ -44,6 +46,7 @@ export const dateLocales = {
'fr-FR': fr, 'fr-FR': fr,
'mn-MN': enUS, 'mn-MN': enUS,
'el-GR': el, 'el-GR': el,
'fo-FO': da,
}; };
export const menuOptions = [ export const menuOptions = [
@ -52,6 +55,7 @@ export const menuOptions = [
{ label: 'Dansk', value: 'da-DK', display: 'da' }, { label: 'Dansk', value: 'da-DK', display: 'da' },
{ label: 'Deutsch', value: 'de-DE', display: 'de' }, { label: 'Deutsch', value: 'de-DE', display: 'de' },
{ label: 'Español', value: 'es-MX', display: 'es' }, { label: 'Español', value: 'es-MX', display: 'es' },
{ label: 'Føroyskt', value: 'fo-FO', display: 'fo' },
{ label: 'Français', value: 'fr-FR', display: 'fr' }, { label: 'Français', value: 'fr-FR', display: 'fr' },
{ label: 'Ελληνικά', value: 'el-GR', display: 'el' }, { label: 'Ελληνικά', value: 'el-GR', display: 'el' },
{ label: '日本語', value: 'ja-JP', display: 'ja' }, { label: '日本語', value: 'ja-JP', display: 'ja' },
@ -59,7 +63,7 @@ export const menuOptions = [
{ label: 'Nederlands', value: 'nl-NL', display: 'nl' }, { label: 'Nederlands', value: 'nl-NL', display: 'nl' },
{ label: 'Русский', value: 'ru-RU', display: 'ru' }, { label: 'Русский', value: 'ru-RU', display: 'ru' },
{ label: 'Svenska', value: 'sv-SE', display: 'sv' }, { label: 'Svenska', value: 'sv-SE', display: 'sv' },
{ label: 'Turkish', value: 'tr-TR', display: 'tr' }, { label: 'Türkçe', value: 'tr-TR', display: 'tr' },
]; ];
export function dateFormat(date, str, locale) { export function dateFormat(date, str, locale) {

View File

@ -1,5 +1,5 @@
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import prisma, { runQuery } from 'lib/db'; import prisma from 'lib/db';
import { subMinutes } from 'date-fns'; import { subMinutes } from 'date-fns';
import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants'; import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants';
@ -15,7 +15,27 @@ export function getDatabase() {
return type; return type;
} }
export function getDateQuery(db, field, unit, timezone) { export async function runQuery(query) {
return query.catch(e => {
throw e;
});
}
export async function rawQuery(query, params) {
const db = getDatabase();
if (db !== POSTGRESQL && db !== MYSQL) {
return Promise.reject(new Error('Unknown database.'));
}
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
return prisma.$queryRaw.apply(prisma, [sql, ...params]);
}
export function getDateQuery(field, unit, timezone) {
const db = getDatabase();
if (db === POSTGRESQL) { if (db === POSTGRESQL) {
if (timezone) { if (timezone) {
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`; return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
@ -34,6 +54,18 @@ export function getDateQuery(db, field, unit, timezone) {
} }
} }
export function getTimestampInterval(field) {
const db = getDatabase();
if (db === POSTGRESQL) {
return `floor(extract(epoch from max(${field}) - min(${field})))`;
}
if (db === MYSQL) {
return `floor(unix_timestamp(max(${field})) - unix_timestamp(min(${field})))`;
}
}
export async function getWebsiteById(website_id) { export async function getWebsiteById(website_id) {
return runQuery( return runQuery(
prisma.website.findOne({ prisma.website.findOne({
@ -253,62 +285,35 @@ export async function createAccount(data) {
); );
} }
export function getMetrics(website_id, start_at, end_at) { export function getMetrics(website_id, start_at, end_at, url) {
const db = getDatabase(); const params = [website_id, start_at, end_at];
let urlFilter = '';
if (db === POSTGRESQL) { if (url) {
return runQuery( urlFilter = `and url=$${params.length + 1}`;
prisma.$queryRaw( params.push(decodeURIComponent(url));
` }
return rawQuery(
`
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 1 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,
${getDateQuery(db, 'created_at', 'hour')}, ${getDateQuery('created_at', 'hour')},
count(*) c, count(*) c,
floor(extract(epoch from max(created_at) - min(created_at))) as "time" ${getTimestampInterval('created_at')} as "time"
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
${urlFilter}
group by 1, 2 group by 1, 2
) t ) t
`, `,
website_id, params,
start_at, );
end_at,
),
);
}
if (db === MYSQL) {
return runQuery(
prisma.$queryRaw(
`
select sum(t.c) as "pageviews",
count(distinct t.session_id) as "uniques",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(t.time) as "totaltime"
from (
select session_id,
${getDateQuery(db, 'created_at', 'hour')},
count(*) c,
floor(unix_timestamp(max(created_at)) - unix_timestamp(min(created_at))) as "time"
from pageview
where website_id=?
and created_at between ? and ?
group by 1, 2
) t
`,
website_id,
start_at,
end_at,
),
);
}
return Promise.reject(new Error('Unknown database.'));
} }
export function getPageviews( export function getPageviews(
@ -318,177 +323,125 @@ export function getPageviews(
timezone = 'utc', timezone = 'utc',
unit = 'day', unit = 'day',
count = '*', count = '*',
url,
) { ) {
const db = getDatabase(); const params = [website_id, start_at, end_at];
let urlFilter = '';
if (db === POSTGRESQL) { if (url) {
return runQuery( urlFilter = `and url=$${params.length + 1}`;
prisma.$queryRaw( params.push(decodeURIComponent(url));
`
select ${getDateQuery(db, 'created_at', unit, timezone)} t,
count(${count}) y
from pageview
where website_id=$1
and created_at between $2 and $3
group by 1
order by 1
`,
website_id,
start_at,
end_at,
),
);
} }
if (db === MYSQL) { return rawQuery(
return runQuery( `
prisma.$queryRaw( select ${getDateQuery('created_at', unit, timezone)} t,
` count(${count}) y
select ${getDateQuery(db, 'created_at', unit, timezone)} t, from pageview
count(${count}) y where website_id=$1
from pageview and created_at between $2 and $3
where website_id=? ${urlFilter}
and created_at between ? and ? group by 1
group by 1 order by 1
order by 1 `,
`, params,
website_id, );
start_at,
end_at,
),
);
}
return Promise.reject(new Error('Unknown database.'));
} }
export function getRankings(website_id, start_at, end_at, type, table, domain) { export function getSessionMetrics(website_id, start_at, end_at, field, url) {
const db = getDatabase(); const params = [website_id, start_at, end_at];
let urlFilter = '';
const filter = domain ? `and ${type} not like '%${domain}%'` : ''; if (url) {
urlFilter = `and url=$${params.length + 1}`;
params.push(decodeURIComponent(url));
}
if (db === POSTGRESQL) { return rawQuery(
return runQuery( `
prisma.$queryRaw( select ${field} x, count(*) y
` from session
select distinct ${type} x, count(*) y where session_id in (
from ${table} select session_id
from pageview
where website_id=$1 where website_id=$1
and created_at between $2 and $3 and created_at between $2 and $3
${filter} ${urlFilter}
group by 1 )
order by 2 desc group by 1
`, order by 2 desc
website_id, `,
start_at, params,
end_at, );
), }
);
export function getPageviewMetrics(website_id, start_at, end_at, field, table, domain, url) {
const params = [website_id, start_at, end_at];
let domainFilter = '';
let urlFilter = '';
if (domain) {
domainFilter = `and referrer not like $${params.length + 1}`;
params.push(`%${domain}%`);
} }
if (db === MYSQL) { if (url) {
return runQuery( urlFilter = `and url=$${params.length + 1}`;
prisma.$queryRaw( params.push(decodeURIComponent(url));
`
select distinct ${type} x, count(*) y
from ${table}
where website_id=?
and created_at between ? and ?
${filter}
group by 1
order by 2 desc
`,
website_id,
start_at,
end_at,
),
);
} }
return Promise.reject(new Error('Unknown database.')); return rawQuery(
`
select ${field} x, count(*) y
from ${table}
where website_id=$1
and created_at between $2 and $3
${domainFilter}
${urlFilter}
group by 1
order by 2 desc
`,
params,
);
} }
export function getActiveVisitors(website_id) { export function getActiveVisitors(website_id) {
const db = getDatabase();
const date = subMinutes(new Date(), 5); const date = subMinutes(new Date(), 5);
const params = [website_id, date];
if (db === POSTGRESQL) { return rawQuery(
return runQuery( `
prisma.$queryRaw(
`
select count(distinct session_id) x select count(distinct session_id) x
from pageview from pageview
where website_id=$1 where website_id=$1
and created_at >= $2 and created_at >= $2
`, `,
website_id, params,
date, );
), }
);
export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit = 'day', url) {
const params = [website_id, start_at, end_at];
let urlFilter = '';
if (url) {
urlFilter = `and url=$${params.length + 1}`;
params.push(decodeURIComponent(url));
} }
if (db === MYSQL) { return rawQuery(
return runQuery( `
prisma.$queryRaw( select
` event_value x,
select count(distinct session_id) x ${getDateQuery('created_at', unit, timezone)} t,
from pageview count(*) y
where website_id=? from event
and created_at >= ? where website_id=$1
and created_at between $2 and $3
${urlFilter}
group by 1, 2
order by 2
`, `,
website_id, params,
date, );
),
);
}
return Promise.reject(new Error('Unknown database.'));
}
export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit = 'day') {
const db = getDatabase();
if (db === POSTGRESQL) {
return runQuery(
prisma.$queryRaw(
`
select
event_value x,
${getDateQuery(db, 'created_at', unit, 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 runQuery(
prisma.$queryRaw(
`
select
event_value x,
${getDateQuery(db, '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.reject(new Error('Unknown database.'));
} }

View File

@ -1,8 +1,9 @@
import path from 'path';
import requestIp from 'request-ip'; import requestIp from 'request-ip';
import { browserName, detectOS } from 'detect-browser'; import { browserName, detectOS } from 'detect-browser';
import isLocalhost from 'is-localhost-ip'; import isLocalhost from 'is-localhost-ip';
import maxmind from 'maxmind'; import maxmind from 'maxmind';
import geolite2 from 'geolite2-redist';
import { import {
DESKTOP_OS, DESKTOP_OS,
MOBILE_OS, MOBILE_OS,
@ -60,15 +61,11 @@ export async function getCountry(req, ip) {
} }
// Database lookup // Database lookup
const lookup = await geolite2.open('GeoLite2-Country', path => { const lookup = await maxmind.open(path.resolve('./public/geo/GeoLite2-Country.mmdb'));
return maxmind.open(path);
});
const result = lookup.get(ip); const result = lookup.get(ip);
lookup.close(); return result?.country?.iso_code;
return result.country.iso_code;
} }
export async function getClientInfo(req, { screen }) { export async function getClientInfo(req, { screen }) {

View File

@ -13,3 +13,18 @@ export function getDomainName(str) {
return str; return str;
} }
} }
export function getQueryString(params = {}) {
const map = Object.keys(params).reduce((arr, key) => {
if (params[key] !== undefined) {
return arr.concat(`${key}=${encodeURIComponent(params[key])}`);
}
return arr;
}, []);
if (map.length) {
return `?${map.join('&')}`;
}
return '';
}

View File

@ -1,7 +1,10 @@
import { getQueryString } from './url';
export const apiRequest = (method, url, body) => export const apiRequest = (method, url, body) =>
fetch(url, { fetch(url, {
method, method,
cache: 'no-cache', cache: 'no-cache',
credentials: 'same-origin',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -19,19 +22,9 @@ export const apiRequest = (method, url, body) =>
return null; return null;
}); });
const parseQuery = (url, params = {}) => { export const get = (url, params) => apiRequest('get', `${url}${getQueryString(params)}`);
const query = Object.keys(params).reduce((values, key) => {
if (params[key] !== undefined) {
return values.concat(`${key}=${encodeURIComponent(params[key])}`);
}
return values;
}, []);
return query.length ? `${url}?${query.join('&')}` : url;
};
export const get = (url, params) => apiRequest('get', parseQuery(url, params)); export const del = (url, params) => apiRequest('delete', `${url}${getQueryString(params)}`);
export const del = (url, params) => apiRequest('delete', parseQuery(url, params));
export const post = (url, params) => apiRequest('post', url, JSON.stringify(params)); export const post = (url, params) => apiRequest('post', url, JSON.stringify(params));

View File

@ -4,6 +4,9 @@ module.exports = {
env: { env: {
VERSION: pkg.version, VERSION: pkg.version,
}, },
serverRuntimeConfig: {
PROJECT_ROOT: __dirname,
},
webpack(config) { webpack(config) {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "0.49.0", "version": "0.58.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",
@ -11,19 +11,20 @@
}, },
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "npm-run-all build-tracker build-lang build-db build-app", "build": "npm-run-all build-tracker build-lang build-geo build-db build-app",
"start": "next start", "start": "next start",
"build-app": "next build", "build-app": "next build",
"build-tracker": "rollup -c rollup.tracker.config.js", "build-tracker": "rollup -c rollup.tracker.config.js",
"build-db": "npm-run-all copy-db-schema build-db-client", "build-db": "npm-run-all copy-db-schema build-db-client",
"build-lang": "npm-run-all format-lang compile-lang", "build-lang": "npm-run-all format-lang compile-lang",
"copy-db-schema": "node scripts/copy-db-schema.js", "build-geo": "node scripts/build-geo.js",
"build-db-schema": "dotenv prisma introspect", "build-db-schema": "dotenv prisma introspect",
"build-db-client": "dotenv prisma generate", "build-db-client": "dotenv prisma generate",
"build-mysql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.mysql.prisma", "build-mysql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.mysql.prisma",
"build-mysql-client": "dotenv prisma generate -- --schema=./prisma/schema.mysql.prisma", "build-mysql-client": "dotenv prisma generate -- --schema=./prisma/schema.mysql.prisma",
"build-postgresql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.postgresql.prisma", "build-postgresql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.postgresql.prisma",
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma", "build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
"copy-db-schema": "node scripts/copy-db-schema.js",
"generate-lang": "npm-run-all extract-lang merge-lang", "generate-lang": "npm-run-all extract-lang merge-lang",
"extract-lang": "formatjs extract {pages,components}/**/*.js --out-file build/messages.json", "extract-lang": "formatjs extract {pages,components}/**/*.js --out-file build/messages.json",
"merge-lang": "node scripts/merge-lang.js", "merge-lang": "node scripts/merge-lang.js",
@ -60,9 +61,7 @@
"date-fns": "^2.16.1", "date-fns": "^2.16.1",
"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",
"formik": "^2.1.5", "formik": "^2.1.5",
"geolite2-redist": "^1.0.7",
"immer": "^7.0.9", "immer": "^7.0.9",
"is-localhost-ip": "^1.4.0", "is-localhost-ip": "^1.4.0",
"isbot-fast": "^1.2.0", "isbot-fast": "^1.2.0",
@ -72,7 +71,7 @@
"next": "^9.5.3", "next": "^9.5.3",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-intl": "^5.8.2", "react-intl": "^5.8.3",
"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",
@ -83,23 +82,24 @@
"request-ip": "^2.1.3", "request-ip": "^2.1.3",
"thenby": "^1.3.4", "thenby": "^1.3.4",
"timezone-support": "^2.0.2", "timezone-support": "^2.0.2",
"tinycolor2": "^1.4.1", "tinycolor2": "^1.4.2",
"uuid": "^8.3.0" "uuid": "^8.3.0"
}, },
"devDependencies": { "devDependencies": {
"@formatjs/cli": "^2.11.3", "@formatjs/cli": "^2.12.0",
"@prisma/cli": "2.7.1", "@prisma/cli": "2.7.1",
"@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-node-resolve": "^9.0.0", "@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-replace": "^2.3.3", "@rollup/plugin-replace": "^2.3.3",
"@svgr/webpack": "^5.4.0", "@svgr/webpack": "^5.4.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"del": "^5.1.0", "del": "^6.0.0",
"dotenv": "^8.2.0",
"dotenv-cli": "^4.0.0", "dotenv-cli": "^4.0.0",
"eslint": "^7.9.0", "eslint": "^7.10.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.12.0",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.6", "eslint-plugin-react": "^7.21.2",
"eslint-plugin-react-hooks": "^4.1.2", "eslint-plugin-react-hooks": "^4.1.2",
"extract-react-intl-messages": "^4.1.1", "extract-react-intl-messages": "^4.1.1",
"husky": "^4.3.0", "husky": "^4.3.0",
@ -110,12 +110,13 @@
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"prettier-eslint": "^11.0.0", "prettier-eslint": "^11.0.0",
"rollup": "^2.28.1", "rollup": "^2.28.2",
"rollup-plugin-hashbang": "^2.2.2", "rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"stylelint": "^13.7.1", "stylelint": "^13.7.1",
"stylelint-config-css-modules": "^2.2.0", "stylelint-config-css-modules": "^2.2.0",
"stylelint-config-prettier": "^8.0.1", "stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^3.0.0" "stylelint-config-recommended": "^3.0.0",
"tar": "^6.0.5"
} }
} }

View File

@ -11,7 +11,7 @@ export default async (req, res) => {
return unauthorized(res); return unauthorized(res);
} }
const { id, start_at, end_at, unit, tz } = req.query; const { id, start_at, end_at, unit, tz, url } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) { if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res); return badRequest(res);
@ -21,7 +21,7 @@ export default async (req, res) => {
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);
const events = await getEvents(websiteId, startDate, endDate, tz, unit); const events = await getEvents(websiteId, startDate, endDate, tz, unit, url);
return ok(res, events); return ok(res, events);
} }

View File

@ -8,13 +8,13 @@ export default async (req, res) => {
return unauthorized(res); return unauthorized(res);
} }
const { id, start_at, end_at } = req.query; const { id, start_at, end_at, url } = req.query;
const websiteId = +id; const websiteId = +id;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);
const metrics = await getMetrics(websiteId, startDate, endDate); const metrics = await getMetrics(websiteId, startDate, endDate, url);
const stats = Object.keys(metrics[0]).reduce((obj, key) => { const stats = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = Number(metrics[0][key]) || 0; obj[key] = Number(metrics[0][key]) || 0;

View File

@ -11,7 +11,7 @@ export default async (req, res) => {
return unauthorized(res); return unauthorized(res);
} }
const { id, start_at, end_at, unit, tz } = req.query; const { id, start_at, end_at, unit, tz, url } = req.query;
const websiteId = +id; const websiteId = +id;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
@ -22,8 +22,8 @@ export default async (req, res) => {
} }
const [pageviews, uniques] = await Promise.all([ const [pageviews, uniques] = await Promise.all([
getPageviews(websiteId, startDate, endDate, tz, unit, '*'), getPageviews(websiteId, startDate, endDate, tz, unit, '*', url),
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'), getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url),
]); ]);
return ok(res, { pageviews, uniques }); return ok(res, { pageviews, uniques });

View File

@ -1,4 +1,4 @@
import { getRankings } from 'lib/queries'; import { getPageviewMetrics, getSessionMetrics } from 'lib/queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import { allowQuery } from 'lib/auth'; import { allowQuery } from 'lib/auth';
@ -31,32 +31,35 @@ export default async (req, res) => {
return unauthorized(res); return unauthorized(res);
} }
const { id, type, start_at, end_at, domain } = req.query; const { id, type, start_at, end_at, domain, url } = req.query;
if (domain && !DOMAIN_REGEX.test(domain)) {
return badRequest(res);
}
const websiteId = +id; const websiteId = +id;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);
if ( if (sessionColumns.includes(type)) {
type !== 'event' && const data = await getSessionMetrics(websiteId, startDate, endDate, type, url);
!sessionColumns.includes(type) &&
!pageviewColumns.includes(type) && return ok(res, data);
domain &&
DOMAIN_REGEX.test(domain)
) {
return badRequest(res);
} }
const rankings = await getRankings( if (type === 'event' || pageviewColumns.includes(type)) {
websiteId, const data = await getPageviewMetrics(
startDate, websiteId,
endDate, startDate,
getColumn(type), endDate,
getTable(type), getColumn(type),
domain, getTable(type),
); domain,
type !== 'url' ? url : undefined,
);
return ok(res, rankings); return ok(res, data);
}
} }
return methodNotAllowed(res); return methodNotAllowed(res);

49
scripts/build-geo.js Normal file
View File

@ -0,0 +1,49 @@
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const https = require('https');
const zlib = require('zlib');
const tar = require('tar');
let url =
'https://raw.githubusercontent.com/GitSquared/node-geolite2-redist/master/redist/GeoLite2-Country.tar.gz';
if (process.env.MAXMIND_LICENSE_KEY) {
url =
`https://download.maxmind.com/app/geoip_download` +
`?edition_id=GeoLite2-Country&license_key=${process.env.MAXMIND_LICENSE_KEY}&suffix=tar.gz`;
}
const dest = path.resolve(__dirname, '../public/geo');
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest);
}
const download = url =>
new Promise(resolve => {
https.get(url, res => {
resolve(res.pipe(zlib.createGunzip({})).pipe(tar.t()));
});
});
download(url).then(
res =>
new Promise((resolve, reject) => {
res.on('entry', entry => {
if (entry.path.endsWith('.mmdb')) {
const filename = path.join(dest, path.basename(entry.path));
entry.pipe(fs.createWriteStream(filename));
console.log('Saved geo database:', filename);
}
});
res.on('error', e => {
reject(e);
});
res.on('finish', () => {
resolve();
});
}),
);

161
yarn.lock
View File

@ -1064,12 +1064,12 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@formatjs/cli@^2.11.3": "@formatjs/cli@^2.12.0":
version "2.11.3" version "2.12.0"
resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-2.11.3.tgz#b06a740520aa0d5345adb2330d13447b4292840c" resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-2.12.0.tgz#f0bb253db073903634e57e587e0395cd0d0cd681"
integrity sha512-7aV3B/16GkHX5Stna9GoKU2o6/At0zxW7PYATRZujf6gbSsIUMHpUZWnMNk0qaJtXeU+3HLsSquRflCaQruoJw== integrity sha512-F0epNBWCXjKGgej8GL1q4RLGqR38bRCPmGLb3VautkbZ74achB0cVGj2w/AdlQiJJ1mU5rEU13pRroukUBZ+GA==
dependencies: dependencies:
"@formatjs/ts-transformer" "^2.10.1" "@formatjs/ts-transformer" "^2.11.0"
"@types/json-stable-stringify" "^1.0.32" "@types/json-stable-stringify" "^1.0.32"
"@types/lodash" "^4.14.150" "@types/lodash" "^4.14.150"
"@types/loud-rejection" "^2.0.0" "@types/loud-rejection" "^2.0.0"
@ -1089,10 +1089,10 @@
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.2.2.tgz#4810bdbd696d3805c535fd0620b7c8f45ab3164f" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.2.2.tgz#4810bdbd696d3805c535fd0620b7c8f45ab3164f"
integrity sha512-mLCoAPGlXCVskb/ojBO6iurGqwo6sZvAl8pRC4N25bz4LPWExAM9LsOo057zN3Br1JxUM3RZHG4YGnVt+nSRYQ== integrity sha512-mLCoAPGlXCVskb/ojBO6iurGqwo6sZvAl8pRC4N25bz4LPWExAM9LsOo057zN3Br1JxUM3RZHG4YGnVt+nSRYQ==
"@formatjs/intl-displaynames@^3.3.8": "@formatjs/intl-displaynames@^3.3.9":
version "3.3.8" version "3.3.9"
resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-3.3.8.tgz#37a3698e135e00908c4314c1085a90d4b9381f61" resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-3.3.9.tgz#18eeb39aa05d4a8b064e520725f9178d0c3f8c50"
integrity sha512-HLoiQFCkwjq1ix7xmLC9DAp0sSXBmD2JuzfHiGKrWU8RIbNcHVzbIL4NgkuXkxvnkADXsK67OtfpuQs8kMZ0qw== integrity sha512-6Ez9Ab9p9bsxCM4OlqsT+R0rmrj5lr6xjIXiCTs/pSDFeiNPQabWDHcBpiGlMRE3zifOwUOFSoi5AGGYMFgetw==
dependencies: dependencies:
"@formatjs/ecma402-abstract" "^1.2.2" "@formatjs/ecma402-abstract" "^1.2.2"
@ -1117,20 +1117,28 @@
dependencies: dependencies:
"@formatjs/ecma402-abstract" "^1.2.2" "@formatjs/ecma402-abstract" "^1.2.2"
"@formatjs/intl@^1.3.2": "@formatjs/intl@^1.3.3":
version "1.3.2" version "1.3.3"
resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.3.2.tgz#95240177ef9a66614ae28193d346c1d65036cefd" resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.3.3.tgz#d59997b7ef832e7c47e8ca50861ff7ff7461d56c"
integrity sha512-Z5z9BwUU9M/a46VUhKfKYnEAE+5/2NkISAstwxgtMJmh/tDHYkPF1A4F4bD+YVNusvLXeot1NDH9bQ072XaMsQ== integrity sha512-gFuCIZEH6o1O2ZF8YlhHJEApRrBarQ7iyqxFp4ujllr/tcjgSxfzF+LSBCPyJ1OQIU98ynOc0XKdrAR2wUd3ow==
dependencies: dependencies:
"@formatjs/ecma402-abstract" "^1.2.2" "@formatjs/ecma402-abstract" "^1.2.2"
"@formatjs/intl-displaynames" "^3.3.8" "@formatjs/intl-displaynames" "^3.3.9"
"@formatjs/intl-listformat" "^4.2.7" "@formatjs/intl-listformat" "^4.2.7"
"@formatjs/intl-relativetimeformat" "^7.2.7" "@formatjs/intl-relativetimeformat" "^7.2.7"
fast-memoize "^2.5.2" fast-memoize "^2.5.2"
intl-messageformat "^9.3.8" intl-messageformat "^9.3.8"
intl-messageformat-parser "^6.0.7" intl-messageformat-parser "^6.0.7"
"@formatjs/ts-transformer@^2.10.1", "@formatjs/ts-transformer@^2.6.0": "@formatjs/ts-transformer@^2.11.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-2.11.0.tgz#90c4b5afae55fd8b8c7ab6aa08ca94a123eb94b9"
integrity sha512-d0++zpEeeCtE+RwbEB+TYw0WnC+jlNniIZu9NcILdgN6LEr9+TRxO+Gz4d7nj3g0D5X1LyNx6P4JI+byGxHqzw==
dependencies:
intl-messageformat-parser "^6.0.7"
typescript "^4.0"
"@formatjs/ts-transformer@^2.6.0":
version "2.10.1" version "2.10.1"
resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-2.10.1.tgz#883d77c37a6752e3491be3ffdd705beb85cfd6f8" resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-2.10.1.tgz#883d77c37a6752e3491be3ffdd705beb85cfd6f8"
integrity sha512-d5zpKWnk52bgA7Xf3KteiJ4r5RnDnIGUoUXD32yFiJVZRpPyUeEcuuyBS/1mxb7UDRpFB2gDqimYlqhjeURozA== integrity sha512-d5zpKWnk52bgA7Xf3KteiJ4r5RnDnIGUoUXD32yFiJVZRpPyUeEcuuyBS/1mxb7UDRpFB2gDqimYlqhjeURozA==
@ -1425,14 +1433,6 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/glob@^7.1.1":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==
dependencies:
"@types/minimatch" "*"
"@types/node" "*"
"@types/hoist-non-react-statics@^3.3.1": "@types/hoist-non-react-statics@^3.3.1":
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@ -1463,11 +1463,6 @@
dependencies: dependencies:
loud-rejection "*" loud-rejection "*"
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/minimist@^1.2.0": "@types/minimist@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
@ -3280,18 +3275,18 @@ define-property@^2.0.2:
is-descriptor "^1.0.2" is-descriptor "^1.0.2"
isobject "^3.0.1" isobject "^3.0.1"
del@^5.1.0: del@^6.0.0:
version "5.1.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7" resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952"
integrity sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA== integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==
dependencies: dependencies:
globby "^10.0.1" globby "^11.0.1"
graceful-fs "^4.2.2" graceful-fs "^4.2.4"
is-glob "^4.0.1" is-glob "^4.0.1"
is-path-cwd "^2.2.0" is-path-cwd "^2.2.0"
is-path-inside "^3.0.1" is-path-inside "^3.0.2"
p-map "^3.0.0" p-map "^4.0.0"
rimraf "^3.0.0" rimraf "^3.0.2"
slash "^3.0.0" slash "^3.0.0"
delegates@^1.0.0: delegates@^1.0.0:
@ -3633,10 +3628,10 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
eslint-config-prettier@^6.11.0: eslint-config-prettier@^6.12.0:
version "6.11.0" version "6.12.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz#9eb2bccff727db1c52104f0b49e87ea46605a0d2"
integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA== integrity sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw==
dependencies: dependencies:
get-stdin "^6.0.0" get-stdin "^6.0.0"
@ -3652,10 +3647,10 @@ eslint-plugin-react-hooks@^4.1.2:
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.2.tgz#2eb53731d11c95826ef7a7272303eabb5c9a271e" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.2.tgz#2eb53731d11c95826ef7a7272303eabb5c9a271e"
integrity sha512-ykUeqkGyUGgwTtk78C0o8UG2fzwmgJ0qxBGPp2WqRKsTwcLuVf01kTDRAtOsd4u6whX2XOC8749n2vPydP82fg== integrity sha512-ykUeqkGyUGgwTtk78C0o8UG2fzwmgJ0qxBGPp2WqRKsTwcLuVf01kTDRAtOsd4u6whX2XOC8749n2vPydP82fg==
eslint-plugin-react@^7.20.6: eslint-plugin-react@^7.21.2:
version "7.20.6" version "7.21.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz#4d7845311a93c463493ccfa0a19c9c5d0fd69f60" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.2.tgz#3bd5d2c4c36d5a0428d0d6dda301ac9a84d681b2"
integrity sha512-kidMTE5HAEBSLu23CUDvj8dc3LdBU0ri1scwHBZjI41oDv4tjsWZKU7MQccFzH1QYPYhsnTF2ovh7JlcIcmxgg== integrity sha512-j3XKvrK3rpBzveKFbgAeGsWb9uz6iUOrR0jixRfjwdFeGSRsXvVTFtHDQYCjsd1/6Z/xvb8Vy3LiI5Reo7fDrg==
dependencies: dependencies:
array-includes "^3.1.1" array-includes "^3.1.1"
array.prototype.flatmap "^1.2.3" array.prototype.flatmap "^1.2.3"
@ -3677,7 +3672,7 @@ eslint-scope@^4.0.3:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-scope@^5.0.0, eslint-scope@^5.1.0: eslint-scope@^5.0.0, eslint-scope@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@ -3747,10 +3742,10 @@ eslint@^6.8.0:
text-table "^0.2.0" text-table "^0.2.0"
v8-compile-cache "^2.0.3" v8-compile-cache "^2.0.3"
eslint@^7.9.0: eslint@^7.10.0:
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.9.0.tgz#522aeccc5c3a19017cf0cb46ebfd660a79acf337" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.10.0.tgz#494edb3e4750fb791133ca379e786a8f648c72b9"
integrity sha512-V6QyhX21+uXp4T+3nrNfI3hQNBDa/P8ga7LoQOenwrlEFXrEnUEE+ok1dMtaS3b6rmLXhT1TkTIsG75HMLbknA== integrity sha512-BDVffmqWl7JJXqCjAK6lWtcQThZB/aP1HXSH1JKwGwv0LQEdvpR7qzNrUT487RM39B5goWuboFad5ovMBmD8yA==
dependencies: dependencies:
"@babel/code-frame" "^7.0.0" "@babel/code-frame" "^7.0.0"
"@eslint/eslintrc" "^0.1.3" "@eslint/eslintrc" "^0.1.3"
@ -3760,7 +3755,7 @@ eslint@^7.9.0:
debug "^4.0.1" debug "^4.0.1"
doctrine "^3.0.0" doctrine "^3.0.0"
enquirer "^2.3.5" enquirer "^2.3.5"
eslint-scope "^5.1.0" eslint-scope "^5.1.1"
eslint-utils "^2.1.0" eslint-utils "^2.1.0"
eslint-visitor-keys "^1.3.0" eslint-visitor-keys "^1.3.0"
espree "^7.3.0" espree "^7.3.0"
@ -3981,7 +3976,7 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.4: fast-glob@^3.1.1, fast-glob@^3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
@ -4260,14 +4255,6 @@ gensync@^1.0.0-beta.1:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
geolite2-redist@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/geolite2-redist/-/geolite2-redist-1.0.7.tgz#98ecd0260115a7c90bc0d49fa96e04f8ab56cddb"
integrity sha512-NrsPDYUU7OVTtZzj5McnBI7b2n8teS+zgX3IXSd3qxF9M2OaDA4SeF1loWzDyzkQzPFr8JHEYYIBTD0sQGQ7ug==
dependencies:
rimraf "^3.0.2"
tar "^6.0.2"
get-own-enumerable-property-symbols@^3.0.0: get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
@ -4355,20 +4342,6 @@ globals@^12.1.0:
dependencies: dependencies:
type-fest "^0.8.1" type-fest "^0.8.1"
globby@^10.0.1:
version "10.0.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543"
integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==
dependencies:
"@types/glob" "^7.1.1"
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.0.3"
glob "^7.1.3"
ignore "^5.1.1"
merge2 "^1.2.3"
slash "^3.0.0"
globby@^11.0.1: globby@^11.0.1:
version "11.0.1" version "11.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
@ -4393,7 +4366,7 @@ gonzales-pe@^4.3.0:
dependencies: dependencies:
minimist "^1.2.5" minimist "^1.2.5"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4:
version "4.2.4" version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
@ -4608,7 +4581,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8: ignore@^5.1.4, ignore@^5.1.8:
version "5.1.8" version "5.1.8"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
@ -4934,7 +4907,7 @@ is-path-cwd@^2.2.0:
resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb"
integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==
is-path-inside@^3.0.1: is-path-inside@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==
@ -5627,7 +5600,7 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
merge2@^1.2.3, merge2@^1.3.0: merge2@^1.3.0:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
@ -7192,14 +7165,14 @@ react-fast-compare@^2.0.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-intl@^5.8.2: react-intl@^5.8.3:
version "5.8.2" version "5.8.3"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.8.2.tgz#6be0f31a6438bf12989f7c8f7f4b5610f36e862f" resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.8.3.tgz#2018aca11a49d2d1b0f602e0dda653a47bf59dfb"
integrity sha512-WiZqh4xTSmc+HlCzJ1b0imIPr7OFx0g7kl6RGAazb7WCtUNhsdX5jBmwk5CplwVPXiBCdytuKmyfX4XTrrvA9A== integrity sha512-ueM7JhbBIi+6FpH6jCrJuKcYpjmFn9UAHA28ojY8LMAL1PowZ/53XGGMvfj32J0/2EIuHQ6vUbi/07kJqe1ksQ==
dependencies: dependencies:
"@formatjs/ecma402-abstract" "^1.2.2" "@formatjs/ecma402-abstract" "^1.2.2"
"@formatjs/intl" "^1.3.2" "@formatjs/intl" "^1.3.3"
"@formatjs/intl-displaynames" "^3.3.8" "@formatjs/intl-displaynames" "^3.3.9"
"@formatjs/intl-listformat" "^4.2.7" "@formatjs/intl-listformat" "^4.2.7"
"@formatjs/intl-relativetimeformat" "^7.2.7" "@formatjs/intl-relativetimeformat" "^7.2.7"
"@types/hoist-non-react-statics" "^3.3.1" "@types/hoist-non-react-statics" "^3.3.1"
@ -7637,7 +7610,7 @@ rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3, rimraf@^2.7.1:
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
rimraf@^3.0.0, rimraf@^3.0.2: rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
@ -7669,10 +7642,10 @@ rollup-plugin-terser@^7.0.2:
serialize-javascript "^4.0.0" serialize-javascript "^4.0.0"
terser "^5.0.0" terser "^5.0.0"
rollup@^2.28.1: rollup@^2.28.2:
version "2.28.1" version "2.28.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.28.1.tgz#ceedca3cdb013c2fa8f22f958a29c203368159ea" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.28.2.tgz#599ec4978144a82d8a8ec3d37670a8440cb04e4b"
integrity sha512-DOtVoqOZt3+FjPJWLU8hDIvBjUylc9s6IZvy76XklxzcLvAQLtVAG/bbhsMhcWnYxC0TKKcf1QQ/tg29zeID0Q== integrity sha512-8txbsFBFLmm9Xdt4ByTOGa9Muonmc8MfNjnGAR8U8scJlF1ZW7AgNZa7aqBXaKtlvnYP/ab++fQIq9dB9NWUbg==
optionalDependencies: optionalDependencies:
fsevents "~2.1.2" fsevents "~2.1.2"
@ -8543,7 +8516,7 @@ tar@^4.4.2:
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
yallist "^3.0.3" yallist "^3.0.3"
tar@^6.0.2: tar@^6.0.5:
version "6.0.5" version "6.0.5"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
@ -8635,10 +8608,10 @@ tiny-warning@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@^1.4.1: tinycolor2@^1.4.2:
version "1.4.1" version "1.4.2"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"