Update realtime chart.

This commit is contained in:
Mike Cao 2020-10-08 23:26:05 -07:00
parent e64a555652
commit fdc92d087b
32 changed files with 240 additions and 58 deletions

View File

@ -1,8 +1,8 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import styles from './ActiveUsers.module.css'; import styles from './ActiveUsers.module.css';
import { FormattedMessage } from 'react-intl';
export default function ActiveUsers({ websiteId, token, className }) { export default function ActiveUsers({ websiteId, token, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 }); const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 });

View File

@ -39,6 +39,8 @@ export default function BarChart({
const w = canvas.current.width; const w = canvas.current.width;
switch (unit) { switch (unit) {
case 'minute':
return dateFormat(d, 'h:mm', locale);
case 'hour': case 'hour':
return dateFormat(d, 'ha', locale); return dateFormat(d, 'ha', locale);
case 'day': case 'day':

View File

@ -26,7 +26,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
data: { datasets }, data: { datasets },
} = chart; } = chart;
datasets[0].data = data.uniques; datasets[0].data = data.sessions;
datasets[0].label = intl.formatMessage({ datasets[0].label = intl.formatMessage({
id: 'metrics.unique-visitors', id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors', defaultMessage: 'Unique visitors',
@ -56,7 +56,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
id: 'metrics.unique-visitors', id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors', defaultMessage: 'Unique visitors',
}), }),
data: data.uniques, data: data.sessions,
lineTension: 0, lineTension: 0,
backgroundColor: colors.visitors.background, backgroundColor: colors.visitors.background,
borderColor: colors.visitors.border, borderColor: colors.visitors.border,

View File

@ -45,14 +45,14 @@ export default function WebsiteChart({
{ onDataLoad, update: [modified] }, { onDataLoad, update: [modified] },
); );
const [pageviews, uniques] = useMemo(() => { const chartData = useMemo(() => {
if (data) { if (data) {
return [ return {
getDateArray(data.pageviews, startDate, endDate, unit), pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
getDateArray(data.uniques, startDate, endDate, unit), sessions: getDateArray(data.sessions, startDate, endDate, unit),
]; };
} }
return [[], []]; return { pageviews: [], sessions: [] };
}, [data]); }, [data]);
function handleCloseFilter() { function handleCloseFilter() {
@ -87,7 +87,7 @@ export default function WebsiteChart({
{error && <ErrorMessage />} {error && <ErrorMessage />}
<PageviewsChart <PageviewsChart
websiteId={websiteId} websiteId={websiteId}
data={{ pageviews, uniques }} data={chartData}
unit={unit} unit={unit}
records={getDateLength(startDate, endDate, unit)} records={getDateLength(startDate, endDate, unit)}
loading={loading} loading={loading}

View File

@ -1,36 +1,101 @@
import React, { useState } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl';
import Page from '../layout/Page'; import { subMinutes, startOfMinute, parseISO, format } from 'date-fns';
import PageHeader from '../layout/PageHeader'; import Page from 'components/layout/Page';
import useFetch from '../../hooks/useFetch'; import PageHeader from 'components/layout/PageHeader';
import DropDown from '../common/DropDown'; import DropDown from 'components/common/DropDown';
import RealtimeChart from '../metrics/RealtimeChart'; import useFetch from 'hooks/useFetch';
import PageviewsChart from '../metrics/PageviewsChart';
import { getDateArray } from '../../lib/date';
export default function TestConsole() { function filterTime(data, time) {
const user = useSelector(state => state.user); return data.filter(({ created_at }) => new Date(created_at).getTime() > time);
}
function mapData(data) {
let last = 0;
const arr = [];
data.reduce((obj, val) => {
const { created_at } = val;
const t = startOfMinute(parseISO(created_at));
if (t.getTime() > last) {
obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
arr.push(obj);
last = t;
} else {
obj.y += 1;
}
return obj;
}, {});
return arr;
}
export default function RealtimeDashboard() {
const [data, setData] = useState();
const [website, setWebsite] = useState(); const [website, setWebsite] = useState();
const { data } = useFetch('/api/websites'); const { data: init, loading } = useFetch('/api/realtime', { type: 'init' });
const { data: updates } = useFetch(
'/api/realtime',
{ type: 'update' },
{ disabled: !init?.token, interval: 5000, headers: { 'x-umami-token': init?.token } },
);
if (!data || !user?.is_admin) { const chartData = useMemo(() => {
if (data) {
const endDate = startOfMinute(new Date());
const startDate = subMinutes(endDate, 30);
const unit = 'minute';
console.log({ data });
return {
pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit),
sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit),
};
}
return { pageviews: [], sessions: [] };
}, [data]);
useEffect(() => {
if (init && !data) {
setData(init.data);
} else if (updates) {
const { pageviews, sessions, events } = updates;
const time = subMinutes(startOfMinute(new Date()), 30).getTime();
setData(state => ({
pageviews: filterTime(state.pageviews, time).concat(pageviews),
sessions: filterTime(state.sessions, time).concat(sessions),
events: filterTime(state.events, time).concat(events),
}));
}
}, [updates, init]);
if (!init || loading || !data) {
return null; return null;
} }
const options = [{ label: 'All websites', value: 0 }].concat( const { websites } = init;
data.map(({ name, website_id }) => ({ label: name, value: website_id })),
); const options = [
{ label: <FormattedMessage id="label.all-websites" defaultMessage="All websites" />, value: 0 },
].concat(websites.map(({ name, website_id }) => ({ label: name, value: website_id })));
const selectedValue = options.find(({ value }) => value === website?.website_id)?.value || 0; const selectedValue = options.find(({ value }) => value === website?.website_id)?.value || 0;
function handleSelect(value) { function handleSelect(value) {
setWebsite(data.find(({ website_id }) => website_id === value)); setWebsite(websites.find(({ website_id }) => website_id === value));
} }
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<div>Real time</div> <div>
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
</div>
<DropDown value={selectedValue} options={options} onChange={handleSelect} /> <DropDown value={selectedValue} options={options} onChange={handleSelect} />
</PageHeader> </PageHeader>
<RealtimeChart websiteId={website?.website_id} /> <PageviewsChart websiteId={website?.website_id} data={chartData} unit="minute" records={30} />
</Page> </Page>
); );
} }

View File

@ -14,14 +14,14 @@ export default function useFetch(url, params = {}, options = {}) {
const keys = Object.keys(params) const keys = Object.keys(params)
.sort() .sort()
.map(key => params[key]); .map(key => params[key]);
const { update = [], onDataLoad = () => {} } = options; const { update = [], onDataLoad = () => {}, disabled, headers } = options;
async function loadData() { async function loadData() {
try { try {
setLoadiing(true); setLoadiing(true);
setError(null); setError(null);
const time = performance.now(); const time = performance.now();
const { data, status } = await get(`${basePath}${url}`, params); const { data, status } = await get(`${basePath}${url}`, params, headers);
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() })); dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
@ -43,7 +43,7 @@ export default function useFetch(url, params = {}, options = {}) {
} }
useEffect(() => { useEffect(() => {
if (url) { if (url && !disabled) {
const { interval, delay = 0 } = options; const { interval, delay = 0 } = options;
setTimeout(() => loadData(), delay); setTimeout(() => loadData(), delay);
@ -54,7 +54,7 @@ export default function useFetch(url, params = {}, options = {}) {
clearInterval(id); clearInterval(id);
}; };
} }
}, [url, ...keys, ...update]); }, [url, disabled, ...keys, ...update]);
return { data, status, error, loading }; return { data, status, error, loading };
} }

View File

@ -18,6 +18,7 @@
"button.view-details": "Vis detajler", "button.view-details": "Vis detajler",
"label.accounts": "Kontoer", "label.accounts": "Kontoer",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all-websites": "All websites",
"label.confirm-password": "Godkendt adgangskode", "label.confirm-password": "Godkendt adgangskode",
"label.current-password": "Nuværende adgangskode", "label.current-password": "Nuværende adgangskode",
"label.custom-range": "Tilpasset interval", "label.custom-range": "Tilpasset interval",
@ -36,6 +37,7 @@
"label.password": "Adgangskode", "label.password": "Adgangskode",
"label.passwords-dont-match": "Adgangskoder matcher ikke", "label.passwords-dont-match": "Adgangskoder matcher ikke",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.required": "Påkrævet", "label.required": "Påkrævet",
"label.settings": "Indstillinger", "label.settings": "Indstillinger",
"label.this-month": "Denne måned", "label.this-month": "Denne måned",

View File

@ -18,6 +18,7 @@
"button.view-details": "Details anzeigen", "button.view-details": "Details anzeigen",
"label.accounts": "Konten", "label.accounts": "Konten",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all-websites": "All websites",
"label.confirm-password": "Passwort wiederholen", "label.confirm-password": "Passwort wiederholen",
"label.current-password": "Derzeitiges Passwort", "label.current-password": "Derzeitiges Passwort",
"label.custom-range": "Benutzerdefinierter Bereich", "label.custom-range": "Benutzerdefinierter Bereich",
@ -36,6 +37,7 @@
"label.password": "Passwort", "label.password": "Passwort",
"label.passwords-dont-match": "Passwörter stimmen nicht überein", "label.passwords-dont-match": "Passwörter stimmen nicht überein",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.required": "Erforderlich", "label.required": "Erforderlich",
"label.settings": "Einstellungen", "label.settings": "Einstellungen",
"label.this-month": "Diesen Monat", "label.this-month": "Diesen Monat",

View File

@ -18,6 +18,7 @@
"button.view-details": "Λεπτομέρειες", "button.view-details": "Λεπτομέρειες",
"label.accounts": "Λογαριασμοί", "label.accounts": "Λογαριασμοί",
"label.administrator": "Διαχειριστής", "label.administrator": "Διαχειριστής",
"label.all-websites": "All websites",
"label.confirm-password": "Επιβεβαίωση κωδικού", "label.confirm-password": "Επιβεβαίωση κωδικού",
"label.current-password": "Τωρινός κωδικός πρόσβασης", "label.current-password": "Τωρινός κωδικός πρόσβασης",
"label.custom-range": "Προσαρμοσμένο εύρος", "label.custom-range": "Προσαρμοσμένο εύρος",
@ -36,6 +37,7 @@
"label.password": "Κωδικός", "label.password": "Κωδικός",
"label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", "label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
"label.profile": "Προφίλ", "label.profile": "Προφίλ",
"label.realtime": "Realtime",
"label.required": "Απαιτείται", "label.required": "Απαιτείται",
"label.settings": "Ρυθμίσεις", "label.settings": "Ρυθμίσεις",
"label.this-month": "Αυτο το μήνα", "label.this-month": "Αυτο το μήνα",

View File

@ -18,6 +18,7 @@
"button.view-details": "View details", "button.view-details": "View details",
"label.accounts": "Accounts", "label.accounts": "Accounts",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all-websites": "All websites",
"label.confirm-password": "Confirm password", "label.confirm-password": "Confirm password",
"label.current-password": "Current password", "label.current-password": "Current password",
"label.custom-range": "Custom range", "label.custom-range": "Custom range",
@ -36,6 +37,7 @@
"label.password": "Password", "label.password": "Password",
"label.passwords-dont-match": "Passwords don't match", "label.passwords-dont-match": "Passwords don't match",
"label.profile": "Profile", "label.profile": "Profile",
"label.realtime": "Realtime",
"label.required": "Required", "label.required": "Required",
"label.settings": "Settings", "label.settings": "Settings",
"label.this-month": "This month", "label.this-month": "This month",

View File

@ -18,6 +18,7 @@
"button.view-details": "Ver detalles", "button.view-details": "Ver detalles",
"label.accounts": "Usuarios", "label.accounts": "Usuarios",
"label.administrator": "Administrador", "label.administrator": "Administrador",
"label.all-websites": "All websites",
"label.confirm-password": "Confirmar contraseña", "label.confirm-password": "Confirmar contraseña",
"label.current-password": "Contraseña actual", "label.current-password": "Contraseña actual",
"label.custom-range": "Custom range", "label.custom-range": "Custom range",
@ -36,6 +37,7 @@
"label.password": "Contraseña", "label.password": "Contraseña",
"label.passwords-dont-match": "Las contraseñas no coinciden", "label.passwords-dont-match": "Las contraseñas no coinciden",
"label.profile": "Perfil", "label.profile": "Perfil",
"label.realtime": "Realtime",
"label.required": "Requerido", "label.required": "Requerido",
"label.settings": "Configuraciones", "label.settings": "Configuraciones",
"label.this-month": "Este mes", "label.this-month": "Este mes",

View File

@ -18,6 +18,7 @@
"button.view-details": "Vís upplýsingar", "button.view-details": "Vís upplýsingar",
"label.accounts": "Brúkarar", "label.accounts": "Brúkarar",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all-websites": "All websites",
"label.confirm-password": "Vátta loyniorð", "label.confirm-password": "Vátta loyniorð",
"label.current-password": "Núverandi loyniorð", "label.current-password": "Núverandi loyniorð",
"label.custom-range": "Tillaga spenni", "label.custom-range": "Tillaga spenni",
@ -36,6 +37,7 @@
"label.password": "Loyniorð", "label.password": "Loyniorð",
"label.passwords-dont-match": "Loyniorðini eru ikki eins", "label.passwords-dont-match": "Loyniorðini eru ikki eins",
"label.profile": "Brúkari", "label.profile": "Brúkari",
"label.realtime": "Realtime",
"label.required": "Krav", "label.required": "Krav",
"label.settings": "Stillingar", "label.settings": "Stillingar",
"label.this-month": "Hendan mánan", "label.this-month": "Hendan mánan",

View File

@ -18,6 +18,7 @@
"button.view-details": "Voir les details", "button.view-details": "Voir les details",
"label.accounts": "Comptes", "label.accounts": "Comptes",
"label.administrator": "Administrateur", "label.administrator": "Administrateur",
"label.all-websites": "All websites",
"label.confirm-password": "Confirmation du mot de passe", "label.confirm-password": "Confirmation du mot de passe",
"label.current-password": "Mot de passe actuel", "label.current-password": "Mot de passe actuel",
"label.custom-range": "Plage personnalisée", "label.custom-range": "Plage personnalisée",
@ -36,6 +37,7 @@
"label.password": "Mot de passe", "label.password": "Mot de passe",
"label.passwords-dont-match": "Les mots de passe ne correspondent pas", "label.passwords-dont-match": "Les mots de passe ne correspondent pas",
"label.profile": "Profile", "label.profile": "Profile",
"label.realtime": "Realtime",
"label.required": "Requis", "label.required": "Requis",
"label.settings": "Paramètres", "label.settings": "Paramètres",
"label.this-month": "Ce mois ci", "label.this-month": "Ce mois ci",

View File

@ -18,6 +18,7 @@
"button.view-details": "Lihat Detil", "button.view-details": "Lihat Detil",
"label.accounts": "Akun", "label.accounts": "Akun",
"label.administrator": "Pengelola", "label.administrator": "Pengelola",
"label.all-websites": "All websites",
"label.confirm-password": "Konfirmasi kata sandi", "label.confirm-password": "Konfirmasi kata sandi",
"label.current-password": "Kata sandi sekarang", "label.current-password": "Kata sandi sekarang",
"label.custom-range": "Rentang khusus", "label.custom-range": "Rentang khusus",
@ -36,6 +37,7 @@
"label.password": "Kata sandi", "label.password": "Kata sandi",
"label.passwords-dont-match": "Kata sandi tidak cocok", "label.passwords-dont-match": "Kata sandi tidak cocok",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.required": "Wajib", "label.required": "Wajib",
"label.settings": "Pengaturan", "label.settings": "Pengaturan",
"label.this-month": "Bulan ini", "label.this-month": "Bulan ini",

View File

@ -18,6 +18,7 @@
"button.view-details": "詳細を見る", "button.view-details": "詳細を見る",
"label.accounts": "アカウント", "label.accounts": "アカウント",
"label.administrator": "管理者", "label.administrator": "管理者",
"label.all-websites": "All websites",
"label.confirm-password": "パスワード(確認)", "label.confirm-password": "パスワード(確認)",
"label.current-password": "現在のパスワード", "label.current-password": "現在のパスワード",
"label.custom-range": "期間を指定する", "label.custom-range": "期間を指定する",
@ -36,6 +37,7 @@
"label.password": "パスワード", "label.password": "パスワード",
"label.passwords-dont-match": "パスワードが一致しません", "label.passwords-dont-match": "パスワードが一致しません",
"label.profile": "プロファイル", "label.profile": "プロファイル",
"label.realtime": "Realtime",
"label.required": "必須", "label.required": "必須",
"label.settings": "設定", "label.settings": "設定",
"label.this-month": "今月", "label.this-month": "今月",

View File

@ -18,6 +18,7 @@
"button.view-details": "Дэлгэрүүлж харах", "button.view-details": "Дэлгэрүүлж харах",
"label.accounts": "Хэрэглэгчид", "label.accounts": "Хэрэглэгчид",
"label.administrator": "Админ", "label.administrator": "Админ",
"label.all-websites": "All websites",
"label.confirm-password": "Шинэ нууц үгээ давтах", "label.confirm-password": "Шинэ нууц үгээ давтах",
"label.current-password": "Ашиглаж буй нууц үг", "label.current-password": "Ашиглаж буй нууц үг",
"label.custom-range": "Дурын хугацаа", "label.custom-range": "Дурын хугацаа",
@ -36,6 +37,7 @@
"label.password": "Нууц үг", "label.password": "Нууц үг",
"label.passwords-dont-match": "Нууц үг тохирохгүй байна", "label.passwords-dont-match": "Нууц үг тохирохгүй байна",
"label.profile": "Бүртгэл", "label.profile": "Бүртгэл",
"label.realtime": "Realtime",
"label.required": "Шаардлагатай", "label.required": "Шаардлагатай",
"label.settings": "Тохиргоо", "label.settings": "Тохиргоо",
"label.this-month": "Энэ сар", "label.this-month": "Энэ сар",

View File

@ -18,6 +18,7 @@
"button.view-details": "Vis detaljer", "button.view-details": "Vis detaljer",
"label.accounts": "Kontoer", "label.accounts": "Kontoer",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all-websites": "All websites",
"label.confirm-password": "Godkjenn passord", "label.confirm-password": "Godkjenn passord",
"label.current-password": "Nåværende passord", "label.current-password": "Nåværende passord",
"label.custom-range": "Egendefinert utvalg", "label.custom-range": "Egendefinert utvalg",
@ -36,6 +37,7 @@
"label.password": "Passord", "label.password": "Passord",
"label.passwords-dont-match": "Passordene er ikke like", "label.passwords-dont-match": "Passordene er ikke like",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.required": "Påkrevd", "label.required": "Påkrevd",
"label.settings": "Innstillinger", "label.settings": "Innstillinger",
"label.this-month": "Denne måneden", "label.this-month": "Denne måneden",

View File

@ -18,6 +18,7 @@
"button.view-details": "Meer details", "button.view-details": "Meer details",
"label.accounts": "Gebruikers", "label.accounts": "Gebruikers",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all-websites": "All websites",
"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",
@ -36,6 +37,7 @@
"label.password": "Wachtwoord", "label.password": "Wachtwoord",
"label.passwords-dont-match": "Wachtwoorden komen niet overeen", "label.passwords-dont-match": "Wachtwoorden komen niet overeen",
"label.profile": "Profiel", "label.profile": "Profiel",
"label.realtime": "Realtime",
"label.required": "Verplicht", "label.required": "Verplicht",
"label.settings": "Instellingen", "label.settings": "Instellingen",
"label.this-month": "Deze maand", "label.this-month": "Deze maand",

View File

@ -18,6 +18,7 @@
"button.view-details": "Ver detalhes", "button.view-details": "Ver detalhes",
"label.accounts": "Contas", "label.accounts": "Contas",
"label.administrator": "Administrador", "label.administrator": "Administrador",
"label.all-websites": "All websites",
"label.confirm-password": "Confirmar palavra-passe", "label.confirm-password": "Confirmar palavra-passe",
"label.current-password": "Palavra-passe atual", "label.current-password": "Palavra-passe atual",
"label.custom-range": "Intervalo personalizado", "label.custom-range": "Intervalo personalizado",
@ -36,6 +37,7 @@
"label.password": "Palavra-passe", "label.password": "Palavra-passe",
"label.passwords-dont-match": "Palavra-passes não correspondem", "label.passwords-dont-match": "Palavra-passes não correspondem",
"label.profile": "Perfil", "label.profile": "Perfil",
"label.realtime": "Realtime",
"label.required": "Obrigatório", "label.required": "Obrigatório",
"label.settings": "Definições", "label.settings": "Definições",
"label.this-month": "Este mês", "label.this-month": "Este mês",

View File

@ -18,6 +18,7 @@
"button.view-details": "Vizualizare detalii", "button.view-details": "Vizualizare detalii",
"label.accounts": "Conturi", "label.accounts": "Conturi",
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all-websites": "All websites",
"label.confirm-password": "Confirmare parolă", "label.confirm-password": "Confirmare parolă",
"label.current-password": "Parola curentă", "label.current-password": "Parola curentă",
"label.custom-range": "Interval personalizat", "label.custom-range": "Interval personalizat",
@ -36,6 +37,7 @@
"label.password": "Parolă", "label.password": "Parolă",
"label.passwords-dont-match": "Parolele nu se potrivesc", "label.passwords-dont-match": "Parolele nu se potrivesc",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.required": "Obligatoriu", "label.required": "Obligatoriu",
"label.settings": "Setări", "label.settings": "Setări",
"label.this-month": "Această lună", "label.this-month": "Această lună",

View File

@ -18,6 +18,7 @@
"button.view-details": "Посмотреть детали", "button.view-details": "Посмотреть детали",
"label.accounts": "Аккаунты", "label.accounts": "Аккаунты",
"label.administrator": "Администратор", "label.administrator": "Администратор",
"label.all-websites": "All websites",
"label.confirm-password": "Подтвердить пароль", "label.confirm-password": "Подтвердить пароль",
"label.current-password": "Текущий пароль", "label.current-password": "Текущий пароль",
"label.custom-range": "Другой период", "label.custom-range": "Другой период",
@ -36,6 +37,7 @@
"label.password": "Пароль", "label.password": "Пароль",
"label.passwords-dont-match": "Пароли не совпадают", "label.passwords-dont-match": "Пароли не совпадают",
"label.profile": "Профиль", "label.profile": "Профиль",
"label.realtime": "Realtime",
"label.required": "Обязательное", "label.required": "Обязательное",
"label.settings": "Настройки", "label.settings": "Настройки",
"label.this-month": "Этот месяц", "label.this-month": "Этот месяц",

View File

@ -18,6 +18,7 @@
"button.view-details": "Visa detaljer", "button.view-details": "Visa detaljer",
"label.accounts": "Konton", "label.accounts": "Konton",
"label.administrator": "Administratör", "label.administrator": "Administratör",
"label.all-websites": "All websites",
"label.confirm-password": "Bekräfta lösenord", "label.confirm-password": "Bekräfta lösenord",
"label.current-password": "Nuvarande lösenord", "label.current-password": "Nuvarande lösenord",
"label.custom-range": "Anpassat urval", "label.custom-range": "Anpassat urval",
@ -36,6 +37,7 @@
"label.password": "Lösenord", "label.password": "Lösenord",
"label.passwords-dont-match": "Lösenorden är inte samma", "label.passwords-dont-match": "Lösenorden är inte samma",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.required": "Krävs", "label.required": "Krävs",
"label.settings": "Inställningar", "label.settings": "Inställningar",
"label.this-month": "Denna månad", "label.this-month": "Denna månad",

View File

@ -18,6 +18,7 @@
"button.view-details": "Detayı incele", "button.view-details": "Detayı incele",
"label.accounts": "Hesaplar", "label.accounts": "Hesaplar",
"label.administrator": "Yönetici", "label.administrator": "Yönetici",
"label.all-websites": "All websites",
"label.confirm-password": "Parolayı onayla", "label.confirm-password": "Parolayı onayla",
"label.current-password": "Mevcut parola", "label.current-password": "Mevcut parola",
"label.custom-range": "Özelleştirilmiş aralık", "label.custom-range": "Özelleştirilmiş aralık",
@ -36,6 +37,7 @@
"label.password": "Parola", "label.password": "Parola",
"label.passwords-dont-match": "Parolalar uyuşmuyor", "label.passwords-dont-match": "Parolalar uyuşmuyor",
"label.profile": "Profil", "label.profile": "Profil",
"label.realtime": "Realtime",
"label.required": "Zorunlu alan", "label.required": "Zorunlu alan",
"label.settings": "Ayarlar", "label.settings": "Ayarlar",
"label.this-month": "Bu ay", "label.this-month": "Bu ay",

View File

@ -18,6 +18,7 @@
"button.view-details": "Переглянути деталі", "button.view-details": "Переглянути деталі",
"label.accounts": "Облікові записи", "label.accounts": "Облікові записи",
"label.administrator": "Адміністратор", "label.administrator": "Адміністратор",
"label.all-websites": "All websites",
"label.confirm-password": "Підтвердити пароль", "label.confirm-password": "Підтвердити пароль",
"label.current-password": "Поточний пароль", "label.current-password": "Поточний пароль",
"label.custom-range": "Довільний період", "label.custom-range": "Довільний період",
@ -36,6 +37,7 @@
"label.password": "Пароль", "label.password": "Пароль",
"label.passwords-dont-match": "Паролі не співпадають", "label.passwords-dont-match": "Паролі не співпадають",
"label.profile": "Профіль", "label.profile": "Профіль",
"label.realtime": "Realtime",
"label.required": "Обов'язкове", "label.required": "Обов'язкове",
"label.settings": "Налаштування", "label.settings": "Налаштування",
"label.this-month": "Поточний місяць", "label.this-month": "Поточний місяць",

View File

@ -18,6 +18,7 @@
"button.view-details": "查看更多", "button.view-details": "查看更多",
"label.accounts": "账户", "label.accounts": "账户",
"label.administrator": "管理员", "label.administrator": "管理员",
"label.all-websites": "All websites",
"label.confirm-password": "确认密码", "label.confirm-password": "确认密码",
"label.current-password": "目前密码", "label.current-password": "目前密码",
"label.custom-range": "自定义时间段", "label.custom-range": "自定义时间段",
@ -36,6 +37,7 @@
"label.password": "密码", "label.password": "密码",
"label.passwords-dont-match": "密码不一致", "label.passwords-dont-match": "密码不一致",
"label.profile": "个人资料", "label.profile": "个人资料",
"label.realtime": "Realtime",
"label.required": "必填", "label.required": "必填",
"label.settings": "设置", "label.settings": "设置",
"label.this-month": "本月", "label.this-month": "本月",

View File

@ -7,6 +7,7 @@ import {
addYears, addYears,
subHours, subHours,
subDays, subDays,
startOfMinute,
startOfHour, startOfHour,
startOfDay, startOfDay,
startOfWeek, startOfWeek,
@ -17,6 +18,7 @@ import {
endOfWeek, endOfWeek,
endOfMonth, endOfMonth,
endOfYear, endOfYear,
differenceInMinutes,
differenceInHours, differenceInHours,
differenceInCalendarDays, differenceInCalendarDays,
differenceInCalendarMonths, differenceInCalendarMonths,
@ -114,6 +116,7 @@ export function getDateFromString(str) {
} }
const dateFuncs = { const dateFuncs = {
minute: [differenceInMinutes, addMinutes, startOfMinute],
hour: [differenceInHours, addHours, startOfHour], hour: [differenceInHours, addHours, startOfHour],
day: [differenceInCalendarDays, addDays, startOfDay], day: [differenceInCalendarDays, addDays, startOfDay],
month: [differenceInCalendarMonths, addMonths, startOfMonth], month: [differenceInCalendarMonths, addMonths, startOfMonth],

View File

@ -166,16 +166,6 @@ export async function createSession(website_id, data) {
); );
} }
export async function getSessionById(session_id) {
return runQuery(
prisma.session.findOne({
where: {
session_id,
},
}),
);
}
export async function getSessionByUuid(session_uuid) { export async function getSessionByUuid(session_uuid) {
return runQuery( return runQuery(
prisma.session.findOne({ prisma.session.findOne({
@ -285,6 +275,57 @@ export async function createAccount(data) {
); );
} }
export async function getSessions(websites, start_at) {
return runQuery(
prisma.session.findMany({
where: {
website: {
website_id: {
in: websites,
},
},
created_at: {
gte: start_at,
},
},
}),
);
}
export async function getPageviews(websites, start_at) {
return runQuery(
prisma.pageview.findMany({
where: {
website: {
website_id: {
in: websites,
},
},
created_at: {
gte: start_at,
},
},
}),
);
}
export async function getEvents(websites, start_at) {
return runQuery(
prisma.event.findMany({
where: {
website: {
website_id: {
in: websites,
},
},
created_at: {
gte: start_at,
},
},
}),
);
}
export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
const params = [website_id, start_at, end_at]; const params = [website_id, start_at, end_at];
const { url } = filters; const { url } = filters;
@ -425,7 +466,7 @@ export function getActiveVisitors(website_id) {
); );
} }
export function getEvents( export function getEventMetrics(
website_id, website_id,
start_at, start_at,
end_at, end_at,

View File

@ -19,13 +19,17 @@ export const apiRequest = (method, url, body, headers) =>
return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data })); return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data }));
}); });
export const get = (url, params) => apiRequest('get', `${url}${getQueryString(params)}`); export const get = (url, params, headers) =>
apiRequest('get', `${url}${getQueryString(params)}`, undefined, headers);
export const del = (url, params) => apiRequest('delete', `${url}${getQueryString(params)}`); export const del = (url, params, headers) =>
apiRequest('delete', `${url}${getQueryString(params)}`, undefined, headers);
export const post = (url, params) => apiRequest('post', url, JSON.stringify(params)); export const post = (url, params, headers) =>
apiRequest('post', url, JSON.stringify(params), headers);
export const put = (url, params) => apiRequest('put', url, JSON.stringify(params)); export const put = (url, params, headers) =>
apiRequest('put', url, JSON.stringify(params), headers);
export const hook = (_this, method, callback) => { export const hook = (_this, method, callback) => {
const orig = _this[method]; const orig = _this[method];

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "0.81.0", "version": "0.82.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",

View File

@ -1,19 +1,48 @@
import { subMinutes } from 'date-fns';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { ok, unauthorized, methodNotAllowed } from 'lib/response'; import { ok, methodNotAllowed, badRequest } from 'lib/response';
import { getEvents, getPageviews, getSessions, getUserWebsites } from 'lib/queries';
import { createToken, parseToken } from 'lib/crypto';
export default async (req, res) => { export default async (req, res) => {
await useAuth(req, res); await useAuth(req, res);
const { is_admin } = req.auth; async function getData(websites, time) {
return Promise.all([
if (!is_admin) { getPageviews(websites, time),
return unauthorized(res); getSessions(websites, time),
getEvents(websites, time),
]);
} }
if (req.method === 'GET') { if (req.method === 'GET') {
const [pageviews, sessions, events] = await Promise.all([[], [], []]); const { type } = req.query;
const { user_id } = req.auth;
return ok(res, { pageviews, sessions, events }); if (type === 'init') {
const websites = await getUserWebsites(user_id);
const ids = websites.map(({ website_id }) => website_id);
const [pageviews, sessions, events] = await getData(ids, subMinutes(new Date(), 30));
const token = await createToken({ websites: ids });
return ok(res, { websites, token, data: { pageviews, sessions, events } });
}
if (type === 'update') {
const token = req.headers['x-umami-token'];
if (!token) {
return badRequest(res);
}
const { websites } = await parseToken(token);
const [pageviews, sessions, events] = await getData(websites, new Date());
return ok(res, { pageviews, sessions, events });
}
return badRequest(res);
} }
return methodNotAllowed(res); return methodNotAllowed(res);

View File

@ -1,5 +1,5 @@
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { getEvents } from 'lib/queries'; import { getEventMetrics } from 'lib/queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth'; import { allowQuery } from 'lib/auth';
@ -21,7 +21,7 @@ export default async (req, res) => {
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);
const events = await getEvents(websiteId, startDate, endDate, tz, unit, { url }); const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, { url });
return ok(res, events); return ok(res, events);
} }

View File

@ -21,12 +21,12 @@ export default async (req, res) => {
return badRequest(res); return badRequest(res);
} }
const [pageviews, uniques] = await Promise.all([ const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url), getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url),
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url), getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url),
]); ]);
return ok(res, { pageviews, uniques }); return ok(res, { pageviews, sessions });
} }
return methodNotAllowed(res); return methodNotAllowed(res);