diff --git a/.gitignore b/.gitignore
index ca0f3c4f..5db4c8fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,8 +16,8 @@
# production
/build
/public/umami.js
+/public/geo
/lang-compiled
-/lang-formatted
# misc
.DS_Store
diff --git a/Dockerfile b/Dockerfile
index 871b8a87..31ea0054 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,41 @@
-FROM node:12.18-alpine
-
+# Build image
+FROM node:12.18-alpine AS build
ARG DATABASE_TYPE
-
ENV DATABASE_URL "postgresql://umami:umami@db:5432/umami" \
DATABASE_TYPE=$DATABASE_TYPE
+WORKDIR /build
-COPY . /app
+RUN yarn config set --home enableTelemetry 0
+COPY package.json yarn.lock /build/
+
+# Install only the production dependencies
+RUN yarn install --production --frozen-lockfile
+
+# Cache these modules for production
+RUN cp -R node_modules/ prod_node_modules/
+
+# Install development dependencies
+RUN yarn install --frozen-lockfile
+
+COPY . /build
+RUN yarn next telemetry disable
+RUN yarn build
+
+# Production image
+FROM node:12.18-alpine AS production
WORKDIR /app
-RUN npm install && npm run build
+# Copy cached dependencies
+COPY --from=build /build/prod_node_modules ./node_modules
+
+# Copy generated Prisma client
+COPY --from=build /build/node_modules/.prisma/ ./node_modules/.prisma/
+
+COPY --from=build /build/yarn.lock /build/package.json ./
+COPY --from=build /build/.next ./.next
+COPY --from=build /build/public ./public
+
+USER node
EXPOSE 3000
-
-CMD ["npm", "start"]
+CMD ["yarn", "start"]
diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js
index e3a7e2c2..b38f55de 100644
--- a/components/WebsiteDetails.js
+++ b/components/WebsiteDetails.js
@@ -1,12 +1,12 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
-import { useRouter } from 'next/router';
import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart';
import WorldMap from 'components/common/WorldMap';
import Page from 'components/layout/Page';
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 styles from './WebsiteDetails.module.css';
import PagesTable from './metrics/PagesTable';
@@ -18,7 +18,7 @@ import CountriesTable from './metrics/CountriesTable';
import EventsTable from './metrics/EventsTable';
import EventsChart from './metrics/EventsChart';
import useFetch from 'hooks/useFetch';
-import Loading from 'components/common/Loading';
+import usePageQuery from 'hooks/usePageQuery';
const views = {
url: PagesTable,
@@ -31,31 +31,27 @@ const views = {
};
export default function WebsiteDetails({ websiteId, token }) {
- const router = useRouter();
const { data } = useFetch(`/api/website/${websiteId}`, { token });
const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState();
const {
- query: { id, view },
- basePath,
- asPath,
- } = router;
-
- const path = `${basePath}/${asPath.split('/')[1]}/${id.join('/')}`;
+ resolve,
+ router,
+ query: { view },
+ } = usePageQuery();
const BackButton = () => (
- }
- size="xsmall"
- onClick={() => router.push(path)}
+ size="small"
>
-
-
-
-
+
+
);
const menuOptions = [
@@ -64,31 +60,31 @@ export default function WebsiteDetails({ websiteId, token }) {
},
{
label: ,
- value: `${path}?view=url`,
+ value: resolve({ view: 'url' }),
},
{
label: ,
- value: `${path}?view=referrer`,
+ value: resolve({ view: 'referrer' }),
},
{
label: ,
- value: `${path}?view=browser`,
+ value: resolve({ view: 'browser' }),
},
{
label: ,
- value: `${path}?view=os`,
+ value: resolve({ view: 'os' }),
},
{
label: ,
- value: `${path}?view=device`,
+ value: resolve({ view: 'device' }),
},
{
label: ,
- value: `${path}?view=country`,
+ value: resolve({ view: 'country' }),
},
{
label: ,
- value: `${path}?view=event`,
+ value: resolve({ view: 'event' }),
},
];
@@ -97,7 +93,6 @@ export default function WebsiteDetails({ websiteId, token }) {
token,
websiteDomain: data?.domain,
limit: 10,
- onExpand: handleExpand,
};
const DetailsComponent = views[view];
@@ -108,10 +103,6 @@ export default function WebsiteDetails({ websiteId, token }) {
}
}
- function handleExpand(value) {
- router.push(`${path}?view=${value}`);
- }
-
if (!data) {
return null;
}
@@ -179,7 +170,7 @@ export default function WebsiteDetails({ websiteId, token }) {
contentClassName={styles.content}
menu={menuOptions}
>
-
+
)}
diff --git a/components/WebsiteDetails.module.css b/components/WebsiteDetails.module.css
index ca80dca0..0e1065c6 100644
--- a/components/WebsiteDetails.module.css
+++ b/components/WebsiteDetails.module.css
@@ -16,7 +16,6 @@
}
.backButton {
- align-self: flex-start;
margin-bottom: 16px;
}
diff --git a/components/WebsiteList.js b/components/WebsiteList.js
index b1819748..0df24877 100644
--- a/components/WebsiteList.js
+++ b/components/WebsiteList.js
@@ -34,9 +34,7 @@ export default function WebsiteList({ userId }) {
}
>
} size="medium" onClick={() => router.push('/settings')}>
-
-
-
+
)}
diff --git a/components/common/Button.js b/components/common/Button.js
index b973b36e..5e92d0d8 100644
--- a/components/common/Button.js
+++ b/components/common/Button.js
@@ -13,7 +13,8 @@ export default function Button({
className,
tooltip,
tooltipId,
- disabled = false,
+ disabled,
+ iconRight,
onClick = () => {},
...props
}) {
@@ -30,14 +31,14 @@ export default function Button({
[styles.action]: variant === 'action',
[styles.danger]: variant === 'danger',
[styles.light]: variant === 'light',
- [styles.disabled]: disabled,
+ [styles.iconRight]: iconRight,
})}
disabled={disabled}
onClick={!disabled ? onClick : null}
{...props}
>
- {icon && }
- {children}
+ {icon && }
+ {children && {children}
}
{tooltip && {tooltip}}
);
diff --git a/components/common/Button.module.css b/components/common/Button.module.css
index 324bbb22..b911095f 100644
--- a/components/common/Button.module.css
+++ b/components/common/Button.module.css
@@ -10,7 +10,6 @@
border: 0;
outline: none;
cursor: pointer;
- white-space: nowrap;
position: relative;
}
@@ -22,12 +21,15 @@
color: var(--gray900);
}
-.large {
- font-size: var(--font-size-large);
+.label {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ max-width: 300px;
}
-.medium {
- font-size: var(--font-size-normal);
+.large {
+ font-size: var(--font-size-large);
}
.small {
@@ -38,7 +40,8 @@
font-size: var(--font-size-xsmall);
}
-.action {
+.action,
+.action:active {
color: var(--gray50);
background: var(--gray900);
}
@@ -47,7 +50,8 @@
background: var(--gray800);
}
-.danger {
+.danger,
+.danger:active {
color: var(--gray50);
background: var(--red500);
}
@@ -56,7 +60,9 @@
background: var(--red400);
}
-.light {
+.light,
+.light:active {
+ color: var(--gray900);
background: transparent;
}
@@ -64,6 +70,19 @@
background: inherit;
}
+.button .icon + * {
+ margin-left: 10px;
+}
+
+.button.iconRight .icon {
+ order: 1;
+ margin-left: 10px;
+}
+
+.button.iconRight .icon + * {
+ margin: 0;
+}
+
.button:disabled {
cursor: default;
color: var(--gray500);
diff --git a/components/common/Icon.module.css b/components/common/Icon.module.css
index 47d0ab0d..5b431668 100644
--- a/components/common/Icon.module.css
+++ b/components/common/Icon.module.css
@@ -5,10 +5,6 @@
vertical-align: middle;
}
-.icon + * {
- margin-left: 10px;
-}
-
.icon svg {
fill: currentColor;
}
diff --git a/components/common/Link.js b/components/common/Link.js
index c3a5fa7e..466e018c 100644
--- a/components/common/Link.js
+++ b/components/common/Link.js
@@ -1,12 +1,23 @@
import React from 'react';
import classNames from 'classnames';
import NextLink from 'next/link';
+import Icon from './Icon';
import styles from './Link.module.css';
-export default function Link({ className, children, ...props }) {
+export default function Link({ className, icon, children, size, iconRight, ...props }) {
return (
- {children}
+
+ {icon && }
+ {children}
+
);
}
diff --git a/components/common/Link.module.css b/components/common/Link.module.css
index 24d8f84c..ea6d281d 100644
--- a/components/common/Link.module.css
+++ b/components/common/Link.module.css
@@ -4,6 +4,8 @@ a.link:visited {
position: relative;
color: var(--gray900);
text-decoration: none;
+ display: inline-flex;
+ align-items: center;
}
a.link:before {
@@ -21,3 +23,28 @@ a.link:hover:before {
width: 100%;
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;
+}
diff --git a/components/layout/Header.module.css b/components/layout/Header.module.css
index 63d68ab9..b7fdc62c 100644
--- a/components/layout/Header.module.css
+++ b/components/layout/Header.module.css
@@ -5,6 +5,9 @@
.title {
font-size: var(--font-size-large);
+ display: flex;
+ align-items: center;
+ line-height: 1.4;
}
.logo {
@@ -26,11 +29,12 @@
.buttons {
display: flex;
justify-content: flex-end;
+ align-items: center;
}
@media only screen and (max-width: 992px) {
.title {
- text-align: center;
+ justify-content: center;
}
.nav {
diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js
index f092e62f..97f9bfbd 100644
--- a/components/metrics/BrowsersTable.js
+++ b/components/metrics/BrowsersTable.js
@@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import { browserFilter } from 'lib/filters';
-export default function BrowsersTable({ websiteId, token, limit, onExpand }) {
+export default function BrowsersTable({ websiteId, token, limit }) {
return (
}
@@ -13,7 +13,6 @@ export default function BrowsersTable({ websiteId, token, limit, onExpand }) {
token={token}
limit={limit}
dataFilter={browserFilter}
- onExpand={onExpand}
/>
);
}
diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js
index 1f516653..58548d06 100644
--- a/components/metrics/CountriesTable.js
+++ b/components/metrics/CountriesTable.js
@@ -3,13 +3,7 @@ import MetricsTable from './MetricsTable';
import { countryFilter, percentFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
-export default function CountriesTable({
- websiteId,
- token,
- limit,
- onDataLoad = () => {},
- onExpand,
-}) {
+export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) {
return (
}
@@ -20,7 +14,6 @@ export default function CountriesTable({
limit={limit}
dataFilter={countryFilter}
onDataLoad={data => onDataLoad(percentFilter(data))}
- onExpand={onExpand}
/>
);
}
diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js
index 85d2bdfd..7d87d1c1 100644
--- a/components/metrics/DevicesTable.js
+++ b/components/metrics/DevicesTable.js
@@ -4,7 +4,7 @@ import { deviceFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
import { getDeviceMessage } from 'components/messages';
-export default function DevicesTable({ websiteId, token, limit, onExpand }) {
+export default function DevicesTable({ websiteId, token, limit }) {
return (
}
@@ -15,7 +15,6 @@ export default function DevicesTable({ websiteId, token, limit, onExpand }) {
limit={limit}
dataFilter={deviceFilter}
renderLabel={({ x }) => getDeviceMessage(x)}
- onExpand={onExpand}
/>
);
}
diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js
index afc3e952..113c6f56 100644
--- a/components/metrics/EventsChart.js
+++ b/components/metrics/EventsChart.js
@@ -6,11 +6,13 @@ import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone';
import { EVENT_COLORS } from 'lib/constants';
+import usePageQuery from '../../hooks/usePageQuery';
export default function EventsChart({ websiteId, token }) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, modified } = dateRange;
const [timezone] = useTimezone();
+ const { query } = usePageQuery();
const { data } = useFetch(
`/api/website/${websiteId}/events`,
@@ -19,6 +21,7 @@ export default function EventsChart({ websiteId, token }) {
end_at: +endDate,
unit,
tz: timezone,
+ url: query.url,
token,
},
{ update: [modified] },
diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js
index 948b9f7a..9a7a09cb 100644
--- a/components/metrics/EventsTable.js
+++ b/components/metrics/EventsTable.js
@@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css';
-export default function EventsTable({ websiteId, token, limit, onExpand, onDataLoad }) {
+export default function EventsTable({ websiteId, token, limit, onDataLoad }) {
return (
}
@@ -13,7 +13,6 @@ export default function EventsTable({ websiteId, token, limit, onExpand, onDataL
token={token}
limit={limit}
renderLabel={({ x }) => }
- onExpand={onExpand}
onDataLoad={onDataLoad}
/>
);
diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js
index cad4c00e..f5d888d4 100644
--- a/components/metrics/MetricsBar.js
+++ b/components/metrics/MetricsBar.js
@@ -5,24 +5,30 @@ import Loading from 'components/common/Loading';
import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
+import usePageQuery from 'hooks/usePageQuery';
import MetricCard from './MetricCard';
import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId, token, className }) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
+ const [format, setFormat] = useState(true);
+ const {
+ query: { url },
+ } = usePageQuery();
+
const { data } = useFetch(
`/api/website/${websiteId}/metrics`,
{
start_at: +startDate,
end_at: +endDate,
+ url,
token,
},
{
update: [modified],
},
);
- const [format, setFormat] = useState(true);
const formatFunc = format ? formatLongNumber : formatNumber;
diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js
index eca58dc2..3ac8a395 100644
--- a/components/metrics/MetricsTable.js
+++ b/components/metrics/MetricsTable.js
@@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames';
-import Button from 'components/common/Button';
+import Link from 'components/common/Link';
import Loading from 'components/common/Loading';
import NoData from 'components/common/NoData';
import useFetch from 'hooks/useFetch';
@@ -11,6 +11,7 @@ import Arrow from 'assets/arrow-right.svg';
import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format';
import useDateRange from 'hooks/useDateRange';
+import usePageQuery from 'hooks/usePageQuery';
import styles from './MetricsTable.module.css';
export default function MetricsTable({
@@ -26,10 +27,15 @@ export default function MetricsTable({
limit,
renderLabel,
onDataLoad = () => {},
- onExpand = () => {},
}) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
+ const {
+ resolve,
+ router,
+ query: { url },
+ } = usePageQuery();
+
const { data } = useFetch(
`/api/website/${websiteId}/rankings`,
{
@@ -37,6 +43,7 @@ export default function MetricsTable({
start_at: +startDate,
end_at: +endDate,
domain: websiteDomain,
+ url,
token,
},
{ onDataLoad, delay: 300, update: [modified] },
@@ -99,12 +106,16 @@ export default function MetricsTable({
)}
- {limit && data.length > limit && (
-
} size="xsmall" onClick={() => onExpand(type)}>
-
-
-
-
+ {limit && (
+
}
+ href={router.pathname}
+ as={resolve({ view: type })}
+ size="small"
+ iconRight
+ >
+
+
)}
>
diff --git a/components/metrics/MetricsTable.module.css b/components/metrics/MetricsTable.module.css
index 34fa77ad..bbba0009 100644
--- a/components/metrics/MetricsTable.module.css
+++ b/components/metrics/MetricsTable.module.css
@@ -1,6 +1,6 @@
.container {
position: relative;
- min-height: 460px;
+ min-height: 430px;
font-size: var(--font-size-small);
display: flex;
flex-direction: column;
@@ -21,6 +21,7 @@
.metric {
font-size: var(--font-size-small);
+ font-weight: 600;
text-align: center;
width: 100px;
cursor: pointer;
@@ -72,8 +73,8 @@
.percent {
position: relative;
width: 50px;
- color: #6e6e6e;
- border-left: 1px solid var(--gray500);
+ color: var(--gray600);
+ border-left: 1px solid var(--gray600);
padding-left: 10px;
z-index: 1;
}
diff --git a/components/metrics/OSTable.js b/components/metrics/OSTable.js
index 14f943a1..63d4739c 100644
--- a/components/metrics/OSTable.js
+++ b/components/metrics/OSTable.js
@@ -3,7 +3,7 @@ import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
-export default function OSTable({ websiteId, token, limit, onExpand }) {
+export default function OSTable({ websiteId, token, limit }) {
return (
}
@@ -13,7 +13,6 @@ export default function OSTable({ websiteId, token, limit, onExpand }) {
token={token}
limit={limit}
dataFilter={osFilter}
- onExpand={onExpand}
/>
);
}
diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js
index ba04f871..0acda811 100644
--- a/components/metrics/PagesTable.js
+++ b/components/metrics/PagesTable.js
@@ -1,13 +1,21 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import Link from 'next/link';
import ButtonGroup from 'components/common/ButtonGroup';
+import ButtonLayout from 'components/layout/ButtonLayout';
import { urlFilter } from 'lib/filters';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
+import usePageQuery from 'hooks/usePageQuery';
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 {
+ resolve,
+ query: { url },
+ } = usePageQuery();
const buttons = [
{
@@ -17,9 +25,24 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, onE
{ label: , value: FILTER_RAW },
];
+ const renderLink = ({ x }) => {
+ return (
+
+
+ {decodeURI(x)}
+
+
+ );
+ };
+
return (
<>
- {!limit && }
+ {showFilters && }
}
type="url"
@@ -29,8 +52,7 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, onE
limit={limit}
dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}
- renderLabel={({ x }) => decodeURI(x)}
- onExpand={onExpand}
+ renderLabel={renderLink}
/>
>
);
diff --git a/components/metrics/PagesTable.module.css b/components/metrics/PagesTable.module.css
new file mode 100644
index 00000000..3c592a74
--- /dev/null
+++ b/components/metrics/PagesTable.module.css
@@ -0,0 +1,8 @@
+body .inactive {
+ color: var(--gray500);
+}
+
+body .active {
+ color: var(--gray900);
+ font-weight: 600;
+}
diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js
index 93552cc5..df7a8c15 100644
--- a/components/metrics/ReferrersTable.js
+++ b/components/metrics/ReferrersTable.js
@@ -6,13 +6,7 @@ import ButtonGroup from 'components/common/ButtonGroup';
import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import ButtonLayout from '../layout/ButtonLayout';
-export default function ReferrersTable({
- websiteId,
- websiteDomain,
- token,
- limit,
- onExpand = () => {},
-}) {
+export default function ReferrersTable({ websiteId, websiteDomain, token, limit, showFilters }) {
const [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [
@@ -39,7 +33,7 @@ export default function ReferrersTable({
return (
<>
- {!limit && }
+ {showFilters && }
}
type="referrer"
@@ -54,7 +48,6 @@ export default function ReferrersTable({
domainOnly: filter === FILTER_DOMAIN_ONLY,
raw: filter === FILTER_RAW,
}}
- onExpand={onExpand}
renderLabel={renderLink}
/>
>
diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js
index ecbd0798..99c03951 100644
--- a/components/metrics/WebsiteChart.js
+++ b/components/metrics/WebsiteChart.js
@@ -5,10 +5,13 @@ import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/common/DateFilter';
import StickyHeader from 'components/helpers/StickyHeader';
+import Button from 'components/common/Button';
import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone';
+import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength } from 'lib/date';
+import Times from 'assets/times.svg';
import styles from './WebsiteChart.module.css';
export default function WebsiteChart({
@@ -22,6 +25,11 @@ export default function WebsiteChart({
const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone();
+ const {
+ router,
+ resolve,
+ query: { url },
+ } = usePageQuery();
const { data, loading } = useFetch(
`/api/website/${websiteId}/pageviews`,
@@ -30,6 +38,7 @@ export default function WebsiteChart({
end_at: +endDate,
unit,
tz: timezone,
+ url,
token,
},
{ onDataLoad, update: [modified] },
@@ -45,6 +54,10 @@ export default function WebsiteChart({
return [[], []];
}, [data]);
+ function handleCloseFilter() {
+ router.push(resolve({ url: undefined }));
+ }
+
return (
<>
@@ -54,6 +67,7 @@ export default function WebsiteChart({
stickyClassName={styles.sticky}
enabled={stickyHeader}
>
+ {url && }
@@ -81,3 +95,13 @@ export default function WebsiteChart({
>
);
}
+
+const PageFilter = ({ url, onClick }) => {
+ return (
+
+ } onClick={onClick} variant="action" iconRight>
+ {url}
+
+
+ );
+};
diff --git a/components/metrics/WebsiteChart.module.css b/components/metrics/WebsiteChart.module.css
index ea0fcaee..29f94670 100644
--- a/components/metrics/WebsiteChart.module.css
+++ b/components/metrics/WebsiteChart.module.css
@@ -36,6 +36,11 @@
align-items: center;
}
+.url {
+ text-align: center;
+ margin-bottom: 10px;
+}
+
@media only screen and (max-width: 992px) {
.filter {
display: block;
diff --git a/components/metrics/WebsiteHeader.js b/components/metrics/WebsiteHeader.js
index 71c0db69..41e5385c 100644
--- a/components/metrics/WebsiteHeader.js
+++ b/components/metrics/WebsiteHeader.js
@@ -4,7 +4,6 @@ import Link from 'components/common/Link';
import PageHeader from 'components/layout/PageHeader';
import RefreshButton from 'components/common/RefreshButton';
import ButtonLayout from 'components/layout/ButtonLayout';
-import Icon from 'components/common/Icon';
import ActiveUsers from './ActiveUsers';
import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteHeader.module.css';
@@ -21,9 +20,11 @@ export default function WebsiteHeader({ websiteId, token, title, showLink = fals
href="/website/[...id]"
as={`/website/${websiteId}/${title}`}
className={styles.link}
+ icon={}
+ size="small"
+ iconRight
>
- } size="small" />
)}
diff --git a/components/metrics/WebsiteHeader.module.css b/components/metrics/WebsiteHeader.module.css
index 71ff5b1b..67d23e61 100644
--- a/components/metrics/WebsiteHeader.module.css
+++ b/components/metrics/WebsiteHeader.module.css
@@ -5,14 +5,9 @@
}
.link {
- font-size: var(--font-size-small);
font-weight: 600;
}
-.link svg {
- margin-left: 10px;
-}
-
@media only screen and (max-width: 576px) {
.active {
display: none;
diff --git a/components/settings/AccountSettings.js b/components/settings/AccountSettings.js
index bf02f4b3..aa206fa9 100644
--- a/components/settings/AccountSettings.js
+++ b/components/settings/AccountSettings.js
@@ -42,14 +42,10 @@ export default function AccountSettings() {
row.username !== 'admin' ? (
} size="small" onClick={() => setEditAccount(row)}>
-
-
-
+
} size="small" onClick={() => setDeleteAccount(row)}>
-
-
-
+
) : null;
@@ -102,9 +98,7 @@ export default function AccountSettings() {
} size="small" onClick={() => setAddAccount(true)}>
-
-
-
+
diff --git a/components/settings/ProfileSettings.js b/components/settings/ProfileSettings.js
index f28226c5..e23c73ed 100644
--- a/components/settings/ProfileSettings.js
+++ b/components/settings/ProfileSettings.js
@@ -29,9 +29,7 @@ export default function ProfileSettings() {
} size="small" onClick={() => setChangePassword(true)}>
-
-
-
+
diff --git a/components/settings/ThemeButton.js b/components/settings/ThemeButton.js
index a31440b7..6f32e23b 100644
--- a/components/settings/ThemeButton.js
+++ b/components/settings/ThemeButton.js
@@ -1,6 +1,5 @@
import React from 'react';
import { useTransition, animated } from 'react-spring';
-import Button from 'components/common/Button';
import useTheme from 'hooks/useTheme';
import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg';
@@ -27,7 +26,7 @@ export default function ThemeButton() {
}
return (
-
} size="small" onClick={() => setDeleteWebsite(row)}>
-
-
-
+
);
@@ -117,9 +113,7 @@ export default function WebsiteSettings() {
}
>
} size="medium" onClick={() => setAddWebsite(true)}>
-
-
-
+
);
@@ -131,9 +125,7 @@ export default function WebsiteSettings() {
} size="small" onClick={() => setAddWebsite(true)}>
-
-
-
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 7a12e83d..06f8ab1d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,7 +12,7 @@ services:
depends_on:
- db
db:
- image: postgres:alpine
+ image: postgres:12-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
diff --git a/hooks/usePageQuery.js b/hooks/usePageQuery.js
new file mode 100644
index 00000000..ced19702
--- /dev/null
+++ b/hooks/usePageQuery.js
@@ -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 };
+}
diff --git a/lang/da-DK.json b/lang/da-DK.json
index 0061f7ff..c4940630 100644
--- a/lang/da-DK.json
+++ b/lang/da-DK.json
@@ -45,7 +45,7 @@
"label.unknown": "Ukendt",
"label.username": "Brugernavn",
"label.websites": "Hjemmesider",
- "message.active-users": "{x} Nuværende brugere {x, plural, one {visitor} other {visitors}}",
+ "message.active-users": "{x} nuværende {x, plural, one {bruger} other {brugere}}",
"message.confirm-delete": "Er du sikker på at du vil slette {target}?",
"message.copied": "Kopieret!",
"message.delete-warning": "Alle tilknyttede data slettes også.",
@@ -58,7 +58,7 @@
"message.no-websites-configured": "Du har ikke konfigureret nogen websteder.",
"message.page-not-found": "Side ikke fundet.",
"message.powered-by": "Drevet af {name}",
- "message.save-success": "Gemt.",
+ "message.save-success": "Gemt!",
"message.share-url": "Dette er den offentligt delings-URL til {target}.",
"message.track-stats": "For at spore statistik for {target} skal du placere følgende kode i {head} sektionen på dit websted.",
"message.type-delete": "Skriv {delete} i boksen nedenfor, for at bekræfte.",
diff --git a/lang/fo-FO.json b/lang/fo-FO.json
new file mode 100644
index 00000000..800f91c7
--- /dev/null
+++ b/lang/fo-FO.json
@@ -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"
+}
diff --git a/lang/mn-MN.json b/lang/mn-MN.json
index 0dd543bf..377c81b1 100644
--- a/lang/mn-MN.json
+++ b/lang/mn-MN.json
@@ -5,15 +5,15 @@
"button.cancel": "Цуцлах",
"button.change-password": "Нууц үг солих",
"button.copy-to-clipboard": "Хуулах",
- "button.date-range": "Date range",
+ "button.date-range": "Хугацааны мужид",
"button.delete": "Устгах",
"button.edit": "Засах",
"button.login": "Нэвтрэх",
"button.more": "Цааш",
- "button.refresh": "Refresh",
- "button.reset": "Reset",
+ "button.refresh": "Сэргээх",
+ "button.reset": "Хуучин хэвд нь оруулах",
"button.save": "Хадгалах",
- "button.single-day": "Single day",
+ "button.single-day": "Нэг өдөр",
"button.view-details": "Дэлгэрүүлж харах",
"label.accounts": "Хэрэглэгчид",
"label.administrator": "Админ",
@@ -21,7 +21,7 @@
"label.current-password": "Ашиглаж буй нууц үг",
"label.custom-range": "Дурын хугацаа",
"label.dashboard": "Хянах самбар",
- "label.default-date-range": "Default date range",
+ "label.default-date-range": "Өгөгдмөл хугацааны муж",
"label.domain": "Домэйн",
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
"label.invalid": "Буруу",
@@ -40,9 +40,9 @@
"label.this-month": "Энэ сар",
"label.this-week": "Энэ долоо хоног",
"label.this-year": "Энэ жил",
- "label.timezone": "Timezone",
+ "label.timezone": "Цагийн бүс",
"label.today": "Өнөөдөр",
- "label.unknown": "Unknown",
+ "label.unknown": "Тодорхойгүй",
"label.username": "Хэрэглэгчийн нэр",
"label.websites": "Вебүүд",
"message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна",
@@ -67,10 +67,10 @@
"metrics.bounce-rate": "Нэг хуудас үзээд гарсан",
"metrics.browsers": "Хөтөч",
"metrics.countries": "Улс",
- "metrics.device.desktop": "Desktop",
- "metrics.device.laptop": "Laptop",
- "metrics.device.mobile": "Mobile",
- "metrics.device.tablet": "Tablet",
+ "metrics.device.desktop": "Суурин компьютер",
+ "metrics.device.laptop": "Зөөврийн компьютер",
+ "metrics.device.mobile": "Утас",
+ "metrics.device.tablet": "Таблет",
"metrics.devices": "Төхөөрөмж",
"metrics.events": "Үйлдэл",
"metrics.filter.combined": "Нэгтгэсэн",
diff --git a/lang/sv-SE.json b/lang/sv-SE.json
index 0a5b8a1d..1a14b60d 100644
--- a/lang/sv-SE.json
+++ b/lang/sv-SE.json
@@ -75,10 +75,10 @@
"metrics.events": "Händelser",
"metrics.filter.combined": "Kombinerade",
"metrics.filter.domain-only": "Endast domän",
- "metrics.filter.raw": "Rå",
+ "metrics.filter.raw": "Rådata",
"metrics.operating-systems": "Operativsystem",
"metrics.page-views": "Sidvisningar",
- "metrics.pages": "Sido",
+ "metrics.pages": "Sidor",
"metrics.referrers": "Hänvisare",
"metrics.unique-visitors": "Unika besökare",
"metrics.views": "Visningar",
diff --git a/lib/date.js b/lib/date.js
index 63bad45e..cdfe322c 100644
--- a/lib/date.js
+++ b/lib/date.js
@@ -100,6 +100,19 @@ export function getDateRangeValues(startDate, endDate) {
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 = {
hour: [differenceInHours, addHours, startOfHour],
day: [differenceInCalendarDays, addDays, startOfDay],
@@ -114,12 +127,7 @@ export function getDateArray(data, startDate, endDate, unit) {
function findData(t) {
const x = data.find(e => {
- if (unit === 'hour') {
- 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 normalize(getDateFromString(e.t)).getTime() === t.getTime();
});
return x?.y || 0;
diff --git a/lib/db.js b/lib/db.js
index e314684e..35948998 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -31,9 +31,3 @@ if (process.env.NODE_ENV === 'production') {
}
export default prisma;
-
-export async function runQuery(query) {
- return query.catch(e => {
- throw e;
- });
-}
diff --git a/lib/filters.js b/lib/filters.js
index ee0a1759..dcbb9907 100644
--- a/lib/filters.js
+++ b/lib/filters.js
@@ -13,17 +13,13 @@ export const urlFilter = (data, { raw }) => {
const cleanUrl = url => {
try {
- const { pathname, search, searchParams } = new URL(url);
+ const { pathname, search } = new URL(url, location.origin);
if (search.startsWith('?/')) {
return `${pathname}${search}`;
}
- const path = removeTrailingSlash(pathname);
- const ref = searchParams.get('ref');
- const query = ref ? `?ref=${ref}` : '';
-
- return `${path}${query}`;
+ return removeTrailingSlash(pathname);
} catch {
return null;
}
@@ -34,7 +30,7 @@ export const urlFilter = (data, { raw }) => {
return obj;
}
- const url = cleanUrl(`http://x${x}`);
+ const url = cleanUrl(x);
if (url) {
if (!obj[url]) {
diff --git a/lib/lang.js b/lib/lang.js
index 066df152..d4415895 100644
--- a/lib/lang.js
+++ b/lib/lang.js
@@ -13,6 +13,7 @@ import mnMNMessages from 'lang-compiled/mn-MN.json';
import daMessages from 'lang-compiled/da-DK.json';
import svMessages from 'lang-compiled/sv-SE.json';
import grMessages from 'lang-compiled/el-GR.json';
+import foMessages from 'lang-compiled/fo-FO.json';
export const messages = {
'en-US': enMessages,
@@ -28,6 +29,7 @@ export const messages = {
'da-DK': daMessages,
'sv-SE': svMessages,
'el-GR': grMessages,
+ 'fo-FO': foMessages,
};
export const dateLocales = {
@@ -44,6 +46,7 @@ export const dateLocales = {
'fr-FR': fr,
'mn-MN': enUS,
'el-GR': el,
+ 'fo-FO': da,
};
export const menuOptions = [
@@ -52,6 +55,7 @@ export const menuOptions = [
{ label: 'Dansk', value: 'da-DK', display: 'da' },
{ label: 'Deutsch', value: 'de-DE', display: 'de' },
{ 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: 'Ελληνικά', value: 'el-GR', display: 'el' },
{ label: '日本語', value: 'ja-JP', display: 'ja' },
@@ -59,7 +63,7 @@ export const menuOptions = [
{ label: 'Nederlands', value: 'nl-NL', display: 'nl' },
{ label: 'Русский', value: 'ru-RU', display: 'ru' },
{ 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) {
diff --git a/lib/queries.js b/lib/queries.js
index 684b30d4..af2b7f5b 100644
--- a/lib/queries.js
+++ b/lib/queries.js
@@ -1,5 +1,5 @@
import moment from 'moment-timezone';
-import prisma, { runQuery } from 'lib/db';
+import prisma from 'lib/db';
import { subMinutes } from 'date-fns';
import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants';
@@ -15,7 +15,27 @@ export function getDatabase() {
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 (timezone) {
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) {
return runQuery(
prisma.website.findOne({
@@ -253,62 +285,35 @@ export async function createAccount(data) {
);
}
-export function getMetrics(website_id, start_at, end_at) {
- const db = getDatabase();
+export function getMetrics(website_id, start_at, end_at, url) {
+ const params = [website_id, start_at, end_at];
+ let urlFilter = '';
- if (db === POSTGRESQL) {
- return runQuery(
- prisma.$queryRaw(
- `
+ if (url) {
+ urlFilter = `and url=$${params.length + 1}`;
+ params.push(decodeURIComponent(url));
+ }
+
+ return rawQuery(
+ `
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')},
+ ${getDateQuery('created_at', 'hour')},
count(*) c,
- floor(extract(epoch from max(created_at) - min(created_at))) as "time"
+ ${getTimestampInterval('created_at')} as "time"
from pageview
where website_id=$1
and created_at between $2 and $3
+ ${urlFilter}
group by 1, 2
) t
- `,
- website_id,
- 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.'));
+ `,
+ params,
+ );
}
export function getPageviews(
@@ -318,177 +323,125 @@ export function getPageviews(
timezone = 'utc',
unit = 'day',
count = '*',
+ url,
) {
- const db = getDatabase();
+ const params = [website_id, start_at, end_at];
+ let urlFilter = '';
- if (db === POSTGRESQL) {
- return runQuery(
- prisma.$queryRaw(
- `
- 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 (url) {
+ urlFilter = `and url=$${params.length + 1}`;
+ params.push(decodeURIComponent(url));
}
- if (db === MYSQL) {
- return runQuery(
- prisma.$queryRaw(
- `
- select ${getDateQuery(db, 'created_at', unit, timezone)} t,
- count(${count}) y
- from pageview
- where website_id=?
- and created_at between ? and ?
- group by 1
- order by 1
- `,
- website_id,
- start_at,
- end_at,
- ),
- );
- }
-
- return Promise.reject(new Error('Unknown database.'));
+ return rawQuery(
+ `
+ select ${getDateQuery('created_at', unit, timezone)} t,
+ count(${count}) y
+ from pageview
+ where website_id=$1
+ and created_at between $2 and $3
+ ${urlFilter}
+ group by 1
+ order by 1
+ `,
+ params,
+ );
}
-export function getRankings(website_id, start_at, end_at, type, table, domain) {
- const db = getDatabase();
+export function getSessionMetrics(website_id, start_at, end_at, field, url) {
+ 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 runQuery(
- prisma.$queryRaw(
- `
- select distinct ${type} x, count(*) y
- from ${table}
+ return rawQuery(
+ `
+ select ${field} x, count(*) y
+ from session
+ where session_id in (
+ select session_id
+ from pageview
where website_id=$1
and created_at between $2 and $3
- ${filter}
- group by 1
- order by 2 desc
- `,
- website_id,
- start_at,
- end_at,
- ),
- );
+ ${urlFilter}
+ )
+ group by 1
+ order by 2 desc
+ `,
+ params,
+ );
+}
+
+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) {
- return runQuery(
- prisma.$queryRaw(
- `
- 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,
- ),
- );
+ if (url) {
+ urlFilter = `and url=$${params.length + 1}`;
+ params.push(decodeURIComponent(url));
}
- 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) {
- const db = getDatabase();
const date = subMinutes(new Date(), 5);
+ const params = [website_id, date];
- if (db === POSTGRESQL) {
- return runQuery(
- prisma.$queryRaw(
- `
+ return rawQuery(
+ `
select count(distinct session_id) x
from pageview
where website_id=$1
and created_at >= $2
`,
- website_id,
- date,
- ),
- );
+ params,
+ );
+}
+
+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 runQuery(
- prisma.$queryRaw(
- `
- select count(distinct session_id) x
- from pageview
- where website_id=?
- and created_at >= ?
+ return rawQuery(
+ `
+ select
+ event_value x,
+ ${getDateQuery('created_at', unit, timezone)} t,
+ count(*) y
+ from event
+ where website_id=$1
+ and created_at between $2 and $3
+ ${urlFilter}
+ group by 1, 2
+ order by 2
`,
- website_id,
- 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.'));
+ params,
+ );
}
diff --git a/lib/request.js b/lib/request.js
index 35658ce5..b30a710d 100644
--- a/lib/request.js
+++ b/lib/request.js
@@ -1,8 +1,9 @@
+import path from 'path';
import requestIp from 'request-ip';
import { browserName, detectOS } from 'detect-browser';
import isLocalhost from 'is-localhost-ip';
import maxmind from 'maxmind';
-import geolite2 from 'geolite2-redist';
+
import {
DESKTOP_OS,
MOBILE_OS,
@@ -60,15 +61,11 @@ export async function getCountry(req, ip) {
}
// Database lookup
- const lookup = await geolite2.open('GeoLite2-Country', path => {
- return maxmind.open(path);
- });
+ const lookup = await maxmind.open(path.resolve('./public/geo/GeoLite2-Country.mmdb'));
const result = lookup.get(ip);
- lookup.close();
-
- return result.country.iso_code;
+ return result?.country?.iso_code;
}
export async function getClientInfo(req, { screen }) {
diff --git a/lib/url.js b/lib/url.js
index d90c390e..500736f9 100644
--- a/lib/url.js
+++ b/lib/url.js
@@ -13,3 +13,18 @@ export function getDomainName(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 '';
+}
diff --git a/lib/web.js b/lib/web.js
index 4081b3ab..a20a09c8 100644
--- a/lib/web.js
+++ b/lib/web.js
@@ -1,7 +1,10 @@
+import { getQueryString } from './url';
+
export const apiRequest = (method, url, body) =>
fetch(url, {
method,
cache: 'no-cache',
+ credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
@@ -19,19 +22,9 @@ export const apiRequest = (method, url, body) =>
return null;
});
-const parseQuery = (url, 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', `${url}${getQueryString(params)}`);
-export const get = (url, params) => apiRequest('get', parseQuery(url, params));
-
-export const del = (url, params) => apiRequest('delete', parseQuery(url, params));
+export const del = (url, params) => apiRequest('delete', `${url}${getQueryString(params)}`);
export const post = (url, params) => apiRequest('post', url, JSON.stringify(params));
diff --git a/next.config.js b/next.config.js
index 0ddfc76a..c1e31d7b 100644
--- a/next.config.js
+++ b/next.config.js
@@ -5,6 +5,9 @@ module.exports = {
env: {
VERSION: pkg.version,
},
+ serverRuntimeConfig: {
+ PROJECT_ROOT: __dirname,
+ },
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
diff --git a/package.json b/package.json
index bc78dc07..114d23ed 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "umami",
- "version": "0.48.0",
+ "version": "0.58.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao ",
"license": "MIT",
@@ -11,19 +11,20 @@
},
"scripts": {
"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",
"build-app": "next build",
"build-tracker": "rollup -c rollup.tracker.config.js",
"build-db": "npm-run-all copy-db-schema build-db-client",
"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-client": "dotenv prisma generate",
"build-mysql-schema": "dotenv prisma introspect -- --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-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",
"extract-lang": "formatjs extract {pages,components}/**/*.js --out-file build/messages.json",
"merge-lang": "node scripts/merge-lang.js",
@@ -60,9 +61,7 @@
"date-fns": "^2.16.1",
"date-fns-tz": "^1.0.10",
"detect-browser": "^5.1.1",
- "dotenv": "^8.2.0",
"formik": "^2.1.5",
- "geolite2-redist": "^1.0.7",
"immer": "^7.0.9",
"is-localhost-ip": "^1.4.0",
"isbot-fast": "^1.2.0",
@@ -72,7 +71,7 @@
"next": "^9.5.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-intl": "^5.8.2",
+ "react-intl": "^5.8.3",
"react-redux": "^7.2.1",
"react-simple-maps": "^2.1.2",
"react-spring": "^8.0.27",
@@ -83,23 +82,24 @@
"request-ip": "^2.1.3",
"thenby": "^1.3.4",
"timezone-support": "^2.0.2",
- "tinycolor2": "^1.4.1",
+ "tinycolor2": "^1.4.2",
"uuid": "^8.3.0"
},
"devDependencies": {
- "@formatjs/cli": "^2.11.3",
+ "@formatjs/cli": "^2.12.0",
"@prisma/cli": "2.7.1",
"@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-replace": "^2.3.3",
"@svgr/webpack": "^5.4.0",
"cross-env": "^7.0.2",
- "del": "^5.1.0",
+ "del": "^6.0.0",
+ "dotenv": "^8.2.0",
"dotenv-cli": "^4.0.0",
- "eslint": "^7.9.0",
- "eslint-config-prettier": "^6.11.0",
+ "eslint": "^7.10.0",
+ "eslint-config-prettier": "^6.12.0",
"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",
"extract-react-intl-messages": "^4.1.1",
"husky": "^4.3.0",
@@ -110,12 +110,13 @@
"postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2",
"prettier-eslint": "^11.0.0",
- "rollup": "^2.28.1",
+ "rollup": "^2.28.2",
"rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.2",
"stylelint": "^13.7.1",
"stylelint-config-css-modules": "^2.2.0",
"stylelint-config-prettier": "^8.0.1",
- "stylelint-config-recommended": "^3.0.0"
+ "stylelint-config-recommended": "^3.0.0",
+ "tar": "^6.0.5"
}
}
diff --git a/pages/api/website/[id]/events.js b/pages/api/website/[id]/events.js
index 4b9a656d..da610f17 100644
--- a/pages/api/website/[id]/events.js
+++ b/pages/api/website/[id]/events.js
@@ -11,7 +11,7 @@ export default async (req, 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)) {
return badRequest(res);
@@ -21,7 +21,7 @@ export default async (req, res) => {
const startDate = new Date(+start_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);
}
diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js
index bb5c977b..f7178bf4 100644
--- a/pages/api/website/[id]/metrics.js
+++ b/pages/api/website/[id]/metrics.js
@@ -8,13 +8,13 @@ export default async (req, 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 startDate = new Date(+start_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) => {
obj[key] = Number(metrics[0][key]) || 0;
diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js
index 016c6646..2191a4c4 100644
--- a/pages/api/website/[id]/pageviews.js
+++ b/pages/api/website/[id]/pageviews.js
@@ -11,7 +11,7 @@ export default async (req, 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 startDate = new Date(+start_at);
@@ -22,8 +22,8 @@ export default async (req, res) => {
}
const [pageviews, uniques] = await Promise.all([
- getPageviews(websiteId, startDate, endDate, tz, unit, '*'),
- getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'),
+ getPageviews(websiteId, startDate, endDate, tz, unit, '*', url),
+ getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url),
]);
return ok(res, { pageviews, uniques });
diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js
index 90a0cd9f..3def9d3a 100644
--- a/pages/api/website/[id]/rankings.js
+++ b/pages/api/website/[id]/rankings.js
@@ -1,4 +1,4 @@
-import { getRankings } from 'lib/queries';
+import { getPageviewMetrics, getSessionMetrics } from 'lib/queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { DOMAIN_REGEX } from 'lib/constants';
import { allowQuery } from 'lib/auth';
@@ -31,32 +31,35 @@ export default async (req, 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 startDate = new Date(+start_at);
const endDate = new Date(+end_at);
- if (
- type !== 'event' &&
- !sessionColumns.includes(type) &&
- !pageviewColumns.includes(type) &&
- domain &&
- DOMAIN_REGEX.test(domain)
- ) {
- return badRequest(res);
+ if (sessionColumns.includes(type)) {
+ const data = await getSessionMetrics(websiteId, startDate, endDate, type, url);
+
+ return ok(res, data);
}
- const rankings = await getRankings(
- websiteId,
- startDate,
- endDate,
- getColumn(type),
- getTable(type),
- domain,
- );
+ if (type === 'event' || pageviewColumns.includes(type)) {
+ const data = await getPageviewMetrics(
+ websiteId,
+ startDate,
+ endDate,
+ getColumn(type),
+ getTable(type),
+ domain,
+ type !== 'url' ? url : undefined,
+ );
- return ok(res, rankings);
+ return ok(res, data);
+ }
}
return methodNotAllowed(res);
diff --git a/scripts/build-geo.js b/scripts/build-geo.js
new file mode 100644
index 00000000..cc66f941
--- /dev/null
+++ b/scripts/build-geo.js
@@ -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();
+ });
+ }),
+);
diff --git a/yarn.lock b/yarn.lock
index 136b534d..caed31d8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1064,12 +1064,12 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
-"@formatjs/cli@^2.11.3":
- version "2.11.3"
- resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-2.11.3.tgz#b06a740520aa0d5345adb2330d13447b4292840c"
- integrity sha512-7aV3B/16GkHX5Stna9GoKU2o6/At0zxW7PYATRZujf6gbSsIUMHpUZWnMNk0qaJtXeU+3HLsSquRflCaQruoJw==
+"@formatjs/cli@^2.12.0":
+ version "2.12.0"
+ resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-2.12.0.tgz#f0bb253db073903634e57e587e0395cd0d0cd681"
+ integrity sha512-F0epNBWCXjKGgej8GL1q4RLGqR38bRCPmGLb3VautkbZ74achB0cVGj2w/AdlQiJJ1mU5rEU13pRroukUBZ+GA==
dependencies:
- "@formatjs/ts-transformer" "^2.10.1"
+ "@formatjs/ts-transformer" "^2.11.0"
"@types/json-stable-stringify" "^1.0.32"
"@types/lodash" "^4.14.150"
"@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"
integrity sha512-mLCoAPGlXCVskb/ojBO6iurGqwo6sZvAl8pRC4N25bz4LPWExAM9LsOo057zN3Br1JxUM3RZHG4YGnVt+nSRYQ==
-"@formatjs/intl-displaynames@^3.3.8":
- version "3.3.8"
- resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-3.3.8.tgz#37a3698e135e00908c4314c1085a90d4b9381f61"
- integrity sha512-HLoiQFCkwjq1ix7xmLC9DAp0sSXBmD2JuzfHiGKrWU8RIbNcHVzbIL4NgkuXkxvnkADXsK67OtfpuQs8kMZ0qw==
+"@formatjs/intl-displaynames@^3.3.9":
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-3.3.9.tgz#18eeb39aa05d4a8b064e520725f9178d0c3f8c50"
+ integrity sha512-6Ez9Ab9p9bsxCM4OlqsT+R0rmrj5lr6xjIXiCTs/pSDFeiNPQabWDHcBpiGlMRE3zifOwUOFSoi5AGGYMFgetw==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.2"
@@ -1117,20 +1117,28 @@
dependencies:
"@formatjs/ecma402-abstract" "^1.2.2"
-"@formatjs/intl@^1.3.2":
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.3.2.tgz#95240177ef9a66614ae28193d346c1d65036cefd"
- integrity sha512-Z5z9BwUU9M/a46VUhKfKYnEAE+5/2NkISAstwxgtMJmh/tDHYkPF1A4F4bD+YVNusvLXeot1NDH9bQ072XaMsQ==
+"@formatjs/intl@^1.3.3":
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.3.3.tgz#d59997b7ef832e7c47e8ca50861ff7ff7461d56c"
+ integrity sha512-gFuCIZEH6o1O2ZF8YlhHJEApRrBarQ7iyqxFp4ujllr/tcjgSxfzF+LSBCPyJ1OQIU98ynOc0XKdrAR2wUd3ow==
dependencies:
"@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-relativetimeformat" "^7.2.7"
fast-memoize "^2.5.2"
intl-messageformat "^9.3.8"
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"
resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-2.10.1.tgz#883d77c37a6752e3491be3ffdd705beb85cfd6f8"
integrity sha512-d5zpKWnk52bgA7Xf3KteiJ4r5RnDnIGUoUXD32yFiJVZRpPyUeEcuuyBS/1mxb7UDRpFB2gDqimYlqhjeURozA==
@@ -1425,14 +1433,6 @@
dependencies:
"@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":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@@ -1463,11 +1463,6 @@
dependencies:
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":
version "1.2.0"
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"
isobject "^3.0.1"
-del@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7"
- integrity sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==
+del@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952"
+ integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==
dependencies:
- globby "^10.0.1"
- graceful-fs "^4.2.2"
+ globby "^11.0.1"
+ graceful-fs "^4.2.4"
is-glob "^4.0.1"
is-path-cwd "^2.2.0"
- is-path-inside "^3.0.1"
- p-map "^3.0.0"
- rimraf "^3.0.0"
+ is-path-inside "^3.0.2"
+ p-map "^4.0.0"
+ rimraf "^3.0.2"
slash "^3.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"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
-eslint-config-prettier@^6.11.0:
- version "6.11.0"
- resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1"
- integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==
+eslint-config-prettier@^6.12.0:
+ version "6.12.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz#9eb2bccff727db1c52104f0b49e87ea46605a0d2"
+ integrity sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw==
dependencies:
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"
integrity sha512-ykUeqkGyUGgwTtk78C0o8UG2fzwmgJ0qxBGPp2WqRKsTwcLuVf01kTDRAtOsd4u6whX2XOC8749n2vPydP82fg==
-eslint-plugin-react@^7.20.6:
- version "7.20.6"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz#4d7845311a93c463493ccfa0a19c9c5d0fd69f60"
- integrity sha512-kidMTE5HAEBSLu23CUDvj8dc3LdBU0ri1scwHBZjI41oDv4tjsWZKU7MQccFzH1QYPYhsnTF2ovh7JlcIcmxgg==
+eslint-plugin-react@^7.21.2:
+ version "7.21.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.2.tgz#3bd5d2c4c36d5a0428d0d6dda301ac9a84d681b2"
+ integrity sha512-j3XKvrK3rpBzveKFbgAeGsWb9uz6iUOrR0jixRfjwdFeGSRsXvVTFtHDQYCjsd1/6Z/xvb8Vy3LiI5Reo7fDrg==
dependencies:
array-includes "^3.1.1"
array.prototype.flatmap "^1.2.3"
@@ -3677,7 +3672,7 @@ eslint-scope@^4.0.3:
esrecurse "^4.1.0"
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"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -3747,10 +3742,10 @@ eslint@^6.8.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
-eslint@^7.9.0:
- version "7.9.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.9.0.tgz#522aeccc5c3a19017cf0cb46ebfd660a79acf337"
- integrity sha512-V6QyhX21+uXp4T+3nrNfI3hQNBDa/P8ga7LoQOenwrlEFXrEnUEE+ok1dMtaS3b6rmLXhT1TkTIsG75HMLbknA==
+eslint@^7.10.0:
+ version "7.10.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.10.0.tgz#494edb3e4750fb791133ca379e786a8f648c72b9"
+ integrity sha512-BDVffmqWl7JJXqCjAK6lWtcQThZB/aP1HXSH1JKwGwv0LQEdvpR7qzNrUT487RM39B5goWuboFad5ovMBmD8yA==
dependencies:
"@babel/code-frame" "^7.0.0"
"@eslint/eslintrc" "^0.1.3"
@@ -3760,7 +3755,7 @@ eslint@^7.9.0:
debug "^4.0.1"
doctrine "^3.0.0"
enquirer "^2.3.5"
- eslint-scope "^5.1.0"
+ eslint-scope "^5.1.1"
eslint-utils "^2.1.0"
eslint-visitor-keys "^1.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"
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"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
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"
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:
version "3.0.2"
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:
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:
version "11.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
@@ -4393,7 +4366,7 @@ gonzales-pe@^4.3.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
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"
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"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
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"
integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==
-is-path-inside@^3.0.1:
+is-path-inside@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
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"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
-merge2@^1.2.3, merge2@^1.3.0:
+merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
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"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
-react-intl@^5.8.2:
- version "5.8.2"
- resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.8.2.tgz#6be0f31a6438bf12989f7c8f7f4b5610f36e862f"
- integrity sha512-WiZqh4xTSmc+HlCzJ1b0imIPr7OFx0g7kl6RGAazb7WCtUNhsdX5jBmwk5CplwVPXiBCdytuKmyfX4XTrrvA9A==
+react-intl@^5.8.3:
+ version "5.8.3"
+ resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.8.3.tgz#2018aca11a49d2d1b0f602e0dda653a47bf59dfb"
+ integrity sha512-ueM7JhbBIi+6FpH6jCrJuKcYpjmFn9UAHA28ojY8LMAL1PowZ/53XGGMvfj32J0/2EIuHQ6vUbi/07kJqe1ksQ==
dependencies:
"@formatjs/ecma402-abstract" "^1.2.2"
- "@formatjs/intl" "^1.3.2"
- "@formatjs/intl-displaynames" "^3.3.8"
+ "@formatjs/intl" "^1.3.3"
+ "@formatjs/intl-displaynames" "^3.3.9"
"@formatjs/intl-listformat" "^4.2.7"
"@formatjs/intl-relativetimeformat" "^7.2.7"
"@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:
glob "^7.1.3"
-rimraf@^3.0.0, rimraf@^3.0.2:
+rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
@@ -7669,10 +7642,10 @@ rollup-plugin-terser@^7.0.2:
serialize-javascript "^4.0.0"
terser "^5.0.0"
-rollup@^2.28.1:
- version "2.28.1"
- resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.28.1.tgz#ceedca3cdb013c2fa8f22f958a29c203368159ea"
- integrity sha512-DOtVoqOZt3+FjPJWLU8hDIvBjUylc9s6IZvy76XklxzcLvAQLtVAG/bbhsMhcWnYxC0TKKcf1QQ/tg29zeID0Q==
+rollup@^2.28.2:
+ version "2.28.2"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.28.2.tgz#599ec4978144a82d8a8ec3d37670a8440cb04e4b"
+ integrity sha512-8txbsFBFLmm9Xdt4ByTOGa9Muonmc8MfNjnGAR8U8scJlF1ZW7AgNZa7aqBXaKtlvnYP/ab++fQIq9dB9NWUbg==
optionalDependencies:
fsevents "~2.1.2"
@@ -8543,7 +8516,7 @@ tar@^4.4.2:
safe-buffer "^5.1.2"
yallist "^3.0.3"
-tar@^6.0.2:
+tar@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
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"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
-tinycolor2@^1.4.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
- integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=
+tinycolor2@^1.4.2:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
+ integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
tmp@^0.0.33:
version "0.0.33"