mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 15:23:38 +01:00
commit
a572441cc5
1
assets/exclamation-triangle.svg
Normal file
1
assets/exclamation-triangle.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M270.2 160h35.5c3.4 0 6.1 2.8 6 6.2l-7.5 196c-.1 3.2-2.8 5.8-6 5.8h-20.5c-3.2 0-5.9-2.5-6-5.8l-7.5-196c-.1-3.4 2.6-6.2 6-6.2zM288 388c-15.5 0-28 12.5-28 28s12.5 28 28 28 28-12.5 28-28-12.5-28-28-28zm281.5 52L329.6 24c-18.4-32-64.7-32-83.2 0L6.5 440c-18.4 31.9 4.6 72 41.6 72H528c36.8 0 60-40 41.5-72zM528 480H48c-12.3 0-20-13.3-13.9-24l240-416c6.1-10.6 21.6-10.7 27.7 0l240 416c6.2 10.6-1.5 24-13.8 24z"/></svg>
|
After Width: | Height: | Size: 482 B |
14
components/common/ErrorMessage.js
Normal file
14
components/common/ErrorMessage.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Icon from './Icon';
|
||||||
|
import Exclamation from 'assets/exclamation-triangle.svg';
|
||||||
|
import styles from './ErrorMessage.module.css';
|
||||||
|
|
||||||
|
export default function ErrorMessage() {
|
||||||
|
return (
|
||||||
|
<div className={styles.error}>
|
||||||
|
<Icon icon={<Exclamation />} className={styles.icon} size="large" />
|
||||||
|
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
components/common/ErrorMessage.module.css
Normal file
13
components/common/ErrorMessage.module.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.error {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
@ -9,7 +9,8 @@ export default function MenuButton({
|
|||||||
icon,
|
icon,
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
menuClassname,
|
buttonClassName,
|
||||||
|
menuClassName,
|
||||||
menuPosition = 'bottom',
|
menuPosition = 'bottom',
|
||||||
menuAlign = 'right',
|
menuAlign = 'right',
|
||||||
onSelect,
|
onSelect,
|
||||||
@ -38,7 +39,7 @@ export default function MenuButton({
|
|||||||
<div className={styles.container} ref={ref}>
|
<div className={styles.container} ref={ref}>
|
||||||
<Button
|
<Button
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={classNames(styles.button, { [styles.open]: showMenu })}
|
className={classNames(styles.button, buttonClassName, { [styles.open]: showMenu })}
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
variant="light"
|
variant="light"
|
||||||
>
|
>
|
||||||
@ -46,7 +47,7 @@ export default function MenuButton({
|
|||||||
</Button>
|
</Button>
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
<Menu
|
<Menu
|
||||||
className={menuClassname}
|
className={menuClassName}
|
||||||
options={options}
|
options={options}
|
||||||
selectedOption={selectedOption}
|
selectedOption={selectedOption}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Loading from 'components/common/Loading';
|
import Loading from 'components/common/Loading';
|
||||||
|
import ErrorMessage from 'components/common/ErrorMessage';
|
||||||
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';
|
||||||
@ -17,7 +18,7 @@ export default function MetricsBar({ websiteId, token, className }) {
|
|||||||
query: { url },
|
query: { url },
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data } = useFetch(
|
const { data, error, loading } = useFetch(
|
||||||
`/api/website/${websiteId}/metrics`,
|
`/api/website/${websiteId}/metrics`,
|
||||||
{
|
{
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
@ -40,9 +41,9 @@ export default function MetricsBar({ websiteId, token, className }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
||||||
{!data ? (
|
{!data && loading && <Loading />}
|
||||||
<Loading />
|
{error && <ErrorMessage />}
|
||||||
) : (
|
{data && !error && (
|
||||||
<>
|
<>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||||
|
@ -13,6 +13,7 @@ import { formatNumber, formatLongNumber } from 'lib/format';
|
|||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import styles from './MetricsTable.module.css';
|
import styles from './MetricsTable.module.css';
|
||||||
|
import ErrorMessage from '../common/ErrorMessage';
|
||||||
|
|
||||||
export default function MetricsTable({
|
export default function MetricsTable({
|
||||||
websiteId,
|
websiteId,
|
||||||
@ -36,7 +37,7 @@ export default function MetricsTable({
|
|||||||
query: { url },
|
query: { url },
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data } = useFetch(
|
const { data, loading, error } = useFetch(
|
||||||
`/api/website/${websiteId}/rankings`,
|
`/api/website/${websiteId}/rankings`,
|
||||||
{
|
{
|
||||||
type,
|
type,
|
||||||
@ -61,7 +62,7 @@ export default function MetricsTable({
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [data, dataFilter, filterOptions]);
|
}, [data, error, dataFilter, filterOptions]);
|
||||||
|
|
||||||
const handleSetFormat = () => setFormat(state => !state);
|
const handleSetFormat = () => setFormat(state => !state);
|
||||||
|
|
||||||
@ -86,8 +87,9 @@ export default function MetricsTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.container, className)}>
|
<div className={classNames(styles.container, className)}>
|
||||||
{!data && <Loading />}
|
{!data && loading && <Loading />}
|
||||||
{data && (
|
{error && <ErrorMessage />}
|
||||||
|
{data && !error && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
|
@ -13,6 +13,7 @@ import usePageQuery from 'hooks/usePageQuery';
|
|||||||
import { getDateArray, getDateLength } from 'lib/date';
|
import { getDateArray, getDateLength } from 'lib/date';
|
||||||
import Times from 'assets/times.svg';
|
import Times from 'assets/times.svg';
|
||||||
import styles from './WebsiteChart.module.css';
|
import styles from './WebsiteChart.module.css';
|
||||||
|
import ErrorMessage from '../common/ErrorMessage';
|
||||||
|
|
||||||
export default function WebsiteChart({
|
export default function WebsiteChart({
|
||||||
websiteId,
|
websiteId,
|
||||||
@ -31,7 +32,7 @@ export default function WebsiteChart({
|
|||||||
query: { url },
|
query: { url },
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data, loading } = useFetch(
|
const { data, loading, error } = useFetch(
|
||||||
`/api/website/${websiteId}/pageviews`,
|
`/api/website/${websiteId}/pageviews`,
|
||||||
{
|
{
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
@ -83,6 +84,7 @@ export default function WebsiteChart({
|
|||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
|
{error && <ErrorMessage />}
|
||||||
<PageviewsChart
|
<PageviewsChart
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
data={{ pageviews, uniques }}
|
data={{ pageviews, uniques }}
|
||||||
|
@ -33,7 +33,7 @@ export default function LanguageButton() {
|
|||||||
icon={<Globe />}
|
icon={<Globe />}
|
||||||
options={menuOptions}
|
options={menuOptions}
|
||||||
value={locale}
|
value={locale}
|
||||||
menuClassname={styles.menu}
|
menuClassName={styles.menu}
|
||||||
renderValue={option => option?.display}
|
renderValue={option => option?.display}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
/>
|
/>
|
||||||
|
@ -25,7 +25,13 @@ export default function useFetch(url, params = {}, options = {}) {
|
|||||||
|
|
||||||
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
||||||
|
|
||||||
setData(data);
|
if (status >= 400) {
|
||||||
|
setError(data);
|
||||||
|
setData(null);
|
||||||
|
} else {
|
||||||
|
setData(data);
|
||||||
|
}
|
||||||
|
|
||||||
setStatus(status);
|
setStatus(status);
|
||||||
onDataLoad(data);
|
onDataLoad(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
14
hooks/useForceSSL.js
Normal file
14
hooks/useForceSSL.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
export default function useForceSSL(enabled) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (enabled && typeof window !== 'undefined' && /^http:\/\//.test(location.href)) {
|
||||||
|
router.push(location.href.replace(/^http:\/\//, 'https://'));
|
||||||
|
}
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -7,22 +7,22 @@
|
|||||||
"button.copy-to-clipboard": "Kopiëer naar klembord",
|
"button.copy-to-clipboard": "Kopiëer naar klembord",
|
||||||
"button.date-range": "Datumbereik",
|
"button.date-range": "Datumbereik",
|
||||||
"button.delete": "Verwijderen",
|
"button.delete": "Verwijderen",
|
||||||
"button.dismiss": "Dismiss",
|
"button.dismiss": "Negeren",
|
||||||
"button.edit": "Bewerken",
|
"button.edit": "Bewerken",
|
||||||
"button.login": "Inloggen",
|
"button.login": "Inloggen",
|
||||||
"button.more": "Toon meer",
|
"button.more": "Toon meer",
|
||||||
"button.refresh": "Vernieuwen",
|
"button.refresh": "Vernieuwen",
|
||||||
"button.reset": "Reset",
|
"button.reset": "Resetten",
|
||||||
"button.save": "Opslaan",
|
"button.save": "Opslaan",
|
||||||
"button.single-day": "Enkele dag",
|
"button.single-day": "Enkele dag",
|
||||||
"button.view-details": "Meer details",
|
"button.view-details": "Meer details",
|
||||||
"label.accounts": "Accounts",
|
"label.accounts": "Gebruikers",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.confirm-password": "Wachtwoord bevestigen",
|
"label.confirm-password": "Wachtwoord bevestigen",
|
||||||
"label.current-password": "Huidig wachtwoord",
|
"label.current-password": "Huidig wachtwoord",
|
||||||
"label.custom-range": "Aangepast bereik",
|
"label.custom-range": "Aangepast bereik",
|
||||||
"label.dashboard": "Dashboard",
|
"label.dashboard": "Overzicht",
|
||||||
"label.default-date-range": "Default date range",
|
"label.default-date-range": "Standaard bereik",
|
||||||
"label.domain": "Domein",
|
"label.domain": "Domein",
|
||||||
"label.enable-share-url": "Sta delen via openbare URL toe",
|
"label.enable-share-url": "Sta delen via openbare URL toe",
|
||||||
"label.invalid": "Ongeldig",
|
"label.invalid": "Ongeldig",
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"label.this-month": "Deze maand",
|
"label.this-month": "Deze maand",
|
||||||
"label.this-week": "Deze week",
|
"label.this-week": "Deze week",
|
||||||
"label.this-year": "Dit jaar",
|
"label.this-year": "Dit jaar",
|
||||||
"label.timezone": "Timezone",
|
"label.timezone": "Tijdzone",
|
||||||
"label.today": "Vandaag",
|
"label.today": "Vandaag",
|
||||||
"label.unknown": "Onbekend",
|
"label.unknown": "Onbekend",
|
||||||
"label.username": "Gebruikersnaam",
|
"label.username": "Gebruikersnaam",
|
||||||
@ -55,7 +55,7 @@
|
|||||||
"message.get-tracking-code": "Tracking code",
|
"message.get-tracking-code": "Tracking code",
|
||||||
"message.go-to-settings": "Naar instellingen",
|
"message.go-to-settings": "Naar instellingen",
|
||||||
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
|
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
|
||||||
"message.new-version-available": "A new version of umami {version} is available!",
|
"message.new-version-available": "Een nieuwe versie van umami {version} is beschikbaar!",
|
||||||
"message.no-data-available": "Geen gegevens beschikbaar.",
|
"message.no-data-available": "Geen gegevens beschikbaar.",
|
||||||
"message.no-websites-configured": "Je hebt geen websites ingesteld.",
|
"message.no-websites-configured": "Je hebt geen websites ingesteld.",
|
||||||
"message.page-not-found": "Pagina niet gevonden.",
|
"message.page-not-found": "Pagina niet gevonden.",
|
||||||
|
@ -16,13 +16,9 @@ export function getDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runQuery(query) {
|
export async function runQuery(query) {
|
||||||
return query
|
return query.catch(e => {
|
||||||
.catch(e => {
|
throw e;
|
||||||
throw e;
|
});
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rawQuery(query, params = []) {
|
export async function rawQuery(query, params = []) {
|
||||||
|
@ -4,9 +4,7 @@ const pkg = require('./package.json');
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
VERSION: pkg.version,
|
VERSION: pkg.version,
|
||||||
},
|
FORCE_SSL: !!process.env.FORCE_SSL,
|
||||||
serverRuntimeConfig: {
|
|
||||||
PROJECT_ROOT: __dirname,
|
|
||||||
},
|
},
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
@ -19,4 +17,17 @@ module.exports = {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/umami.js',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Cache-Control',
|
||||||
|
value: 'public, max-age=2592000', // 30 days
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "0.74.0",
|
"version": "0.80.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",
|
||||||
|
@ -3,6 +3,7 @@ import { IntlProvider } from 'react-intl';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { useStore } from 'redux/store';
|
import { useStore } from 'redux/store';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
|
import useForceSSL from 'hooks/useForceSSL';
|
||||||
import { messages } from 'lib/lang';
|
import { messages } from 'lib/lang';
|
||||||
import 'styles/variables.css';
|
import 'styles/variables.css';
|
||||||
import 'styles/bootstrap-grid.css';
|
import 'styles/bootstrap-grid.css';
|
||||||
@ -21,6 +22,7 @@ const Intl = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
|
useForceSSL(process.env.FORCE_SSL);
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -3,12 +3,22 @@ import { savePageView, saveEvent } from 'lib/queries';
|
|||||||
import { useCors, useSession } from 'lib/middleware';
|
import { useCors, useSession } from 'lib/middleware';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest } from 'lib/response';
|
||||||
import { createToken } from 'lib/crypto';
|
import { createToken } from 'lib/crypto';
|
||||||
|
import { getIpAddress } from '../../lib/request';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
if (isBot(req.headers['user-agent'])) {
|
if (isBot(req.headers['user-agent'])) {
|
||||||
return ok(res);
|
return ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.IGNORE_IP) {
|
||||||
|
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim());
|
||||||
|
const ip = getIpAddress(req);
|
||||||
|
|
||||||
|
if (ips.includes(ip)) {
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
await useSession(req, res);
|
await useSession(req, res);
|
||||||
|
|
||||||
|
@ -19,8 +19,19 @@ import { removeTrailingSlash } from '../lib/url';
|
|||||||
const autoTrack = attr('data-auto-track') !== 'false';
|
const autoTrack = attr('data-auto-track') !== 'false';
|
||||||
const dnt = attr('data-do-not-track');
|
const dnt = attr('data-do-not-track');
|
||||||
const useCache = attr('data-cache');
|
const useCache = attr('data-cache');
|
||||||
|
const domains = attr('data-domains');
|
||||||
|
|
||||||
if (!script || (dnt && doNotTrack())) return;
|
if (
|
||||||
|
!script ||
|
||||||
|
(dnt && doNotTrack()) ||
|
||||||
|
(domains &&
|
||||||
|
!domains
|
||||||
|
.split(',')
|
||||||
|
.map(n => n.trim())
|
||||||
|
.includes(hostname))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const root = hostUrl
|
const root = hostUrl
|
||||||
? removeTrailingSlash(hostUrl)
|
? removeTrailingSlash(hostUrl)
|
||||||
|
Loading…
Reference in New Issue
Block a user