diff --git a/assets/exclamation-triangle.svg b/assets/exclamation-triangle.svg new file mode 100644 index 00000000..46bef5bc --- /dev/null +++ b/assets/exclamation-triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/common/ErrorMessage.js b/components/common/ErrorMessage.js new file mode 100644 index 00000000..5747f226 --- /dev/null +++ b/components/common/ErrorMessage.js @@ -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 ( +
+ } className={styles.icon} size="large" /> + +
+ ); +} diff --git a/components/common/ErrorMessage.module.css b/components/common/ErrorMessage.module.css new file mode 100644 index 00000000..232b5f84 --- /dev/null +++ b/components/common/ErrorMessage.module.css @@ -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; +} diff --git a/components/common/MenuButton.js b/components/common/MenuButton.js index 4f3f2584..f3de66d0 100644 --- a/components/common/MenuButton.js +++ b/components/common/MenuButton.js @@ -9,7 +9,8 @@ export default function MenuButton({ icon, value, options, - menuClassname, + buttonClassName, + menuClassName, menuPosition = 'bottom', menuAlign = 'right', onSelect, @@ -38,7 +39,7 @@ export default function MenuButton({
{showMenu && ( - {!data ? ( - - ) : ( + {!data && loading && } + {error && } + {data && !error && ( <> } diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 3ac8a395..6850a3bf 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -13,6 +13,7 @@ import { formatNumber, formatLongNumber } from 'lib/format'; import useDateRange from 'hooks/useDateRange'; import usePageQuery from 'hooks/usePageQuery'; import styles from './MetricsTable.module.css'; +import ErrorMessage from '../common/ErrorMessage'; export default function MetricsTable({ websiteId, @@ -36,7 +37,7 @@ export default function MetricsTable({ query: { url }, } = usePageQuery(); - const { data } = useFetch( + const { data, loading, error } = useFetch( `/api/website/${websiteId}/rankings`, { type, @@ -61,7 +62,7 @@ export default function MetricsTable({ return items; } return []; - }, [data, dataFilter, filterOptions]); + }, [data, error, dataFilter, filterOptions]); const handleSetFormat = () => setFormat(state => !state); @@ -86,8 +87,9 @@ export default function MetricsTable({ return (
- {!data && } - {data && ( + {!data && loading && } + {error && } + {data && !error && ( <>
{title}
diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index ea86ad3e..6a07afe5 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -13,6 +13,7 @@ import usePageQuery from 'hooks/usePageQuery'; import { getDateArray, getDateLength } from 'lib/date'; import Times from 'assets/times.svg'; import styles from './WebsiteChart.module.css'; +import ErrorMessage from '../common/ErrorMessage'; export default function WebsiteChart({ websiteId, @@ -31,7 +32,7 @@ export default function WebsiteChart({ query: { url }, } = usePageQuery(); - const { data, loading } = useFetch( + const { data, loading, error } = useFetch( `/api/website/${websiteId}/pageviews`, { start_at: +startDate, @@ -83,6 +84,7 @@ export default function WebsiteChart({
+ {error && } } options={menuOptions} value={locale} - menuClassname={styles.menu} + menuClassName={styles.menu} renderValue={option => option?.display} onSelect={handleSelect} /> diff --git a/hooks/useFetch.js b/hooks/useFetch.js index 907f45af..0eb82b13 100644 --- a/hooks/useFetch.js +++ b/hooks/useFetch.js @@ -25,7 +25,13 @@ export default function useFetch(url, params = {}, options = {}) { dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() })); - setData(data); + if (status >= 400) { + setError(data); + setData(null); + } else { + setData(data); + } + setStatus(status); onDataLoad(data); } catch (e) { diff --git a/hooks/useForceSSL.js b/hooks/useForceSSL.js new file mode 100644 index 00000000..b9d95e19 --- /dev/null +++ b/hooks/useForceSSL.js @@ -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; +} diff --git a/lang/nl-NL.json b/lang/nl-NL.json index 148fd32e..54f7894f 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -7,22 +7,22 @@ "button.copy-to-clipboard": "Kopiƫer naar klembord", "button.date-range": "Datumbereik", "button.delete": "Verwijderen", - "button.dismiss": "Dismiss", + "button.dismiss": "Negeren", "button.edit": "Bewerken", "button.login": "Inloggen", "button.more": "Toon meer", "button.refresh": "Vernieuwen", - "button.reset": "Reset", + "button.reset": "Resetten", "button.save": "Opslaan", "button.single-day": "Enkele dag", "button.view-details": "Meer details", - "label.accounts": "Accounts", + "label.accounts": "Gebruikers", "label.administrator": "Administrator", "label.confirm-password": "Wachtwoord bevestigen", "label.current-password": "Huidig wachtwoord", "label.custom-range": "Aangepast bereik", - "label.dashboard": "Dashboard", - "label.default-date-range": "Default date range", + "label.dashboard": "Overzicht", + "label.default-date-range": "Standaard bereik", "label.domain": "Domein", "label.enable-share-url": "Sta delen via openbare URL toe", "label.invalid": "Ongeldig", @@ -41,7 +41,7 @@ "label.this-month": "Deze maand", "label.this-week": "Deze week", "label.this-year": "Dit jaar", - "label.timezone": "Timezone", + "label.timezone": "Tijdzone", "label.today": "Vandaag", "label.unknown": "Onbekend", "label.username": "Gebruikersnaam", @@ -55,7 +55,7 @@ "message.get-tracking-code": "Tracking code", "message.go-to-settings": "Naar instellingen", "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-websites-configured": "Je hebt geen websites ingesteld.", "message.page-not-found": "Pagina niet gevonden.", diff --git a/lib/queries.js b/lib/queries.js index 7c0bb6e7..3b138faa 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -16,13 +16,9 @@ export function getDatabase() { } export async function runQuery(query) { - return query - .catch(e => { - throw e; - }) - .finally(async () => { - await prisma.$disconnect(); - }); + return query.catch(e => { + throw e; + }); } export async function rawQuery(query, params = []) { diff --git a/next.config.js b/next.config.js index c1e31d7b..e98c091c 100644 --- a/next.config.js +++ b/next.config.js @@ -4,9 +4,7 @@ const pkg = require('./package.json'); module.exports = { env: { VERSION: pkg.version, - }, - serverRuntimeConfig: { - PROJECT_ROOT: __dirname, + FORCE_SSL: !!process.env.FORCE_SSL, }, webpack(config) { config.module.rules.push({ @@ -19,4 +17,17 @@ module.exports = { return config; }, + async headers() { + return [ + { + source: '/umami.js', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=2592000', // 30 days + }, + ], + }, + ] + }, }; diff --git a/package.json b/package.json index 9c6e899e..aca8f8c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.74.0", + "version": "0.80.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", diff --git a/pages/_app.js b/pages/_app.js index 9aad4339..2849d2f0 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -3,6 +3,7 @@ import { IntlProvider } from 'react-intl'; import { Provider } from 'react-redux'; import { useStore } from 'redux/store'; import useLocale from 'hooks/useLocale'; +import useForceSSL from 'hooks/useForceSSL'; import { messages } from 'lib/lang'; import 'styles/variables.css'; import 'styles/bootstrap-grid.css'; @@ -21,6 +22,7 @@ const Intl = ({ children }) => { }; export default function App({ Component, pageProps }) { + useForceSSL(process.env.FORCE_SSL); const store = useStore(); return ( diff --git a/pages/api/collect.js b/pages/api/collect.js index e6b3e0ca..4f01b225 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -3,12 +3,22 @@ import { savePageView, saveEvent } from 'lib/queries'; import { useCors, useSession } from 'lib/middleware'; import { ok, badRequest } from 'lib/response'; import { createToken } from 'lib/crypto'; +import { getIpAddress } from '../../lib/request'; export default async (req, res) => { if (isBot(req.headers['user-agent'])) { 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 useSession(req, res); diff --git a/tracker/index.js b/tracker/index.js index 154e0034..df4c5216 100644 --- a/tracker/index.js +++ b/tracker/index.js @@ -19,8 +19,19 @@ import { removeTrailingSlash } from '../lib/url'; const autoTrack = attr('data-auto-track') !== 'false'; const dnt = attr('data-do-not-track'); 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 ? removeTrailingSlash(hostUrl)