Merge branch 'umami-software:dev' into dev

This commit is contained in:
Matthias Kretschmann 2023-09-02 11:34:04 +01:00 committed by GitHub
commit 02db7185ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 506 additions and 479 deletions

View File

@ -12,8 +12,8 @@ RUN yarn install --frozen-lockfile
FROM node:18-alpine AS builder FROM node:18-alpine AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY docker/middleware.js .
COPY . . COPY . .
COPY docker/middleware.js ./src
ARG DATABASE_TYPE ARG DATABASE_TYPE
ARG BASE_PATH ARG BASE_PATH

View File

@ -10,7 +10,7 @@ A detailed getting started guide can be found at [https://umami.is/docs/](https:
### Requirements ### Requirements
- A server with Node.js version 12 or newer - A server with Node.js version 16.13 or newer
- A database. Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/) databases. - A database. Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/) databases.
### Install Yarn ### Install Yarn

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "2.6.0", "version": "2.6.2",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.", "description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",

File diff suppressed because it is too large Load Diff

View File

@ -104,7 +104,7 @@
"label.browser": [ "label.browser": [
{ {
"type": 0, "type": 0,
"value": "Browser" "value": "浏览器"
} }
], ],
"label.browsers": [ "label.browsers": [
@ -134,7 +134,7 @@
"label.city": [ "label.city": [
{ {
"type": 0, "type": 0,
"value": "City" "value": "市/县"
} }
], ],
"label.clear-all": [ "label.clear-all": [
@ -176,7 +176,7 @@
"label.country": [ "label.country": [
{ {
"type": 0, "type": 0,
"value": "Country" "value": "国家/地区"
} }
], ],
"label.create-report": [ "label.create-report": [
@ -230,7 +230,7 @@
"label.date": [ "label.date": [
{ {
"type": 0, "type": 0,
"value": "Date" "value": "日期"
} }
], ],
"label.date-range": [ "label.date-range": [
@ -242,7 +242,7 @@
"label.day": [ "label.day": [
{ {
"type": 0, "type": 0,
"value": "Day" "value": ""
} }
], ],
"label.default-date-range": [ "label.default-date-range": [
@ -296,7 +296,7 @@
"label.device": [ "label.device": [
{ {
"type": 0, "type": 0,
"value": "Device" "value": "设备"
} }
], ],
"label.devices": [ "label.devices": [
@ -440,13 +440,13 @@
"label.is-not-set": [ "label.is-not-set": [
{ {
"type": 0, "type": 0,
"value": "Is not set" "value": "未设置"
} }
], ],
"label.is-set": [ "label.is-set": [
{ {
"type": 0, "type": 0,
"value": "Is set" "value": "已设置"
} }
], ],
"label.join": [ "label.join": [
@ -576,7 +576,7 @@
"label.my-websites": [ "label.my-websites": [
{ {
"type": 0, "type": 0,
"value": "My websites" "value": "我的网站"
} }
], ],
"label.name": [ "label.name": [
@ -618,7 +618,15 @@
"label.page-of": [ "label.page-of": [
{ {
"type": 0, "type": 0,
"value": "Page " "value": "总"
},
{
"type": 1,
"value": "total"
},
{
"type": 0,
"value": "中的第"
}, },
{ {
"type": 1, "type": 1,
@ -626,11 +634,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " of " "value": "页"
},
{
"type": 1,
"value": "total"
} }
], ],
"label.page-views": [ "label.page-views": [
@ -642,7 +646,7 @@
"label.pageTitle": [ "label.pageTitle": [
{ {
"type": 0, "type": 0,
"value": "Page title" "value": "标题"
} }
], ],
"label.pages": [ "label.pages": [
@ -704,7 +708,7 @@
"label.referrer": [ "label.referrer": [
{ {
"type": 0, "type": 0,
"value": "Referrer" "value": "来源"
} }
], ],
"label.referrers": [ "label.referrers": [
@ -728,7 +732,7 @@
"label.region": [ "label.region": [
{ {
"type": 0, "type": 0,
"value": "Region" "value": "州/省"
} }
], ],
"label.regions": [ "label.regions": [
@ -770,7 +774,7 @@
"label.retention": [ "label.retention": [
{ {
"type": 0, "type": 0,
"value": "Retention" "value": "保留"
} }
], ],
"label.role": [ "label.role": [
@ -872,7 +876,7 @@
"label.team-name": [ "label.team-name": [
{ {
"type": 0, "type": 0,
"value": "Team name" "value": "团队名称"
} }
], ],
"label.team-owner": [ "label.team-owner": [
@ -884,7 +888,7 @@
"label.team-websites": [ "label.team-websites": [
{ {
"type": 0, "type": 0,
"value": "Team websites" "value": "团队网站"
} }
], ],
"label.teams": [ "label.teams": [
@ -974,7 +978,7 @@
"label.unique": [ "label.unique": [
{ {
"type": 0, "type": 0,
"value": "Unique" "value": "独立"
} }
], ],
"label.unique-visitors": [ "label.unique-visitors": [
@ -998,13 +1002,13 @@
"label.url": [ "label.url": [
{ {
"type": 0, "type": 0,
"value": "URL" "value": "网址"
} }
], ],
"label.urls": [ "label.urls": [
{ {
"type": 0, "type": 0,
"value": "URLs" "value": "网址"
} }
], ],
"label.user": [ "label.user": [
@ -1046,7 +1050,7 @@
"label.view-only": [ "label.view-only": [
{ {
"type": 0, "type": 0,
"value": "View only" "value": "仅浏览量"
} }
], ],
"label.views": [ "label.views": [
@ -1190,15 +1194,15 @@
"message.event-log": [ "message.event-log": [
{ {
"type": 1, "type": 1,
"value": "event" "value": "url"
}, },
{ {
"type": 0, "type": 0,
"value": " on " "value": "上的"
}, },
{ {
"type": 1, "type": 1,
"value": "url" "value": "event"
} }
], ],
"message.go-to-settings": [ "message.go-to-settings": [

View File

@ -1,4 +1,5 @@
import { useEffect, useCallback, useState } from 'react'; import { useEffect, useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
import { Button, Row, Column } from 'react-basics'; import { Button, Row, Column } from 'react-basics';
import { setItem } from 'next-basics'; import { setItem } from 'next-basics';
import useStore, { checkVersion } from 'store/version'; import useStore, { checkVersion } from 'store/version';
@ -44,7 +45,7 @@ export function UpdateNotice({ user, config }) {
return null; return null;
} }
return ( return createPortal(
<Row className={styles.notice}> <Row className={styles.notice}>
<Column variant="two" className={styles.message}> <Column variant="two" className={styles.message}>
{formatMessage(messages.newVersionAvailable, { version: `v${latest}` })} {formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}
@ -55,7 +56,8 @@ export function UpdateNotice({ user, config }) {
</Button> </Button>
<Button onClick={handleDismissClick}>{formatMessage(labels.dismiss)}</Button> <Button onClick={handleDismissClick}>{formatMessage(labels.dismiss)}</Button>
</Column> </Column>
</Row> </Row>,
document.body,
); );
} }

View File

@ -2,13 +2,14 @@
position: absolute; position: absolute;
max-width: 800px; max-width: 800px;
gap: 20px; gap: 20px;
margin: 20px auto; margin: 80px auto;
justify-self: center; align-self: center;
background: var(--base50); background: var(--base50);
padding: 20px; padding: 20px;
border: 1px solid var(--base300); border: 1px solid var(--base300);
border-radius: var(--border-radius); border-radius: var(--border-radius);
z-index: var(--z-index-popup); z-index: var(--z-index-popup);
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1);
} }
.message { .message {

View File

@ -1,8 +1,8 @@
import { useState } from 'react'; import { Button, Icon, Icons, Text } from 'react-basics';
import { Button, Icon, Icons, Text, Flexbox } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import Pager from 'components/common/Pager';
import WebsiteChartList from 'components/pages/websites/WebsiteChartList'; import WebsiteChartList from 'components/pages/websites/WebsiteChartList';
import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton'; import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton';
import DashboardEdit from 'components/pages/dashboard/DashboardEdit'; import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
@ -11,23 +11,24 @@ import useApi from 'components/hooks/useApi';
import useDashboard from 'store/dashboard'; import useDashboard from 'store/dashboard';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import useLocale from 'components/hooks/useLocale'; import useLocale from 'components/hooks/useLocale';
import useApiFilter from 'components/hooks/useApiFilter';
export function Dashboard() { export function Dashboard() {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const dashboard = useDashboard(); const { showCharts, editing } = useDashboard();
const { showCharts, limit, editing } = dashboard;
const [max, setMax] = useState(limit);
const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites'], () =>
get('/websites', { includeTeams: 1 }),
);
const hasData = data && data?.data.length !== 0;
const { dir } = useLocale(); const { dir } = useLocale();
const { get, useQuery } = useApi();
function handleMore() { const { page, handlePageChange } = useApiFilter();
setMax(max + limit); const pageSize = 10;
} const {
data: result,
isLoading,
error,
} = useQuery(['websites', page, pageSize], () =>
get('/websites', { includeTeams: 1, page, pageSize }),
);
const { data, count } = result || {};
const hasData = data && data?.length !== 0;
return ( return (
<Page loading={isLoading} error={error}> <Page loading={isLoading} error={error}>
@ -48,19 +49,17 @@ export function Dashboard() {
)} )}
{hasData && ( {hasData && (
<> <>
{editing && <DashboardEdit websites={data?.data} />} {editing && <DashboardEdit />}
{!editing && ( {!editing && (
<WebsiteChartList websites={data?.data} showCharts={showCharts} limit={max} /> <>
)} <WebsiteChartList websites={data} showCharts={showCharts} limit={pageSize} />
{max < data.length && ( <Pager
<Flexbox justifyContent="center"> page={page}
<Button onClick={handleMore}> pageSize={pageSize}
<Icon rotate={dir === 'rtl' ? 180 : 0}> count={count}
<Icons.More /> onPageChange={handlePageChange}
</Icon> />
<Text>{formatMessage(labels.more)}</Text> </>
</Button>
</Flexbox>
)} )}
</> </>
)} )}

View File

@ -5,23 +5,33 @@ import { Button } from 'react-basics';
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import useDashboard, { saveDashboard } from 'store/dashboard'; import useDashboard, { saveDashboard } from 'store/dashboard';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import useApi from 'components/hooks/useApi';
import styles from './DashboardEdit.module.css'; import styles from './DashboardEdit.module.css';
import Page from 'components/layout/Page';
const dragId = 'dashboard-website-ordering'; const dragId = 'dashboard-website-ordering';
export function DashboardEdit({ websites }) { export function DashboardEdit() {
const settings = useDashboard(); const settings = useDashboard();
const { websiteOrder } = settings; const { websiteOrder } = settings;
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [order, setOrder] = useState(websiteOrder || []); const [order, setOrder] = useState(websiteOrder || []);
const { get, useQuery } = useApi();
const {
data: result,
isLoading,
error,
} = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
const { data: websites } = result || {};
const ordered = useMemo( const ordered = useMemo(() => {
() => if (websites) {
websites return websites
.map(website => ({ ...website, order: order.indexOf(website.id) })) .map(website => ({ ...website, order: order.indexOf(website.id) }))
.sort(firstBy('order')), .sort(firstBy('order'));
[websites, order], }
); return [];
}, [websites, order]);
function handleWebsiteDrag({ destination, source }) { function handleWebsiteDrag({ destination, source }) {
if (!destination || destination.index === source.index) return; if (!destination || destination.index === source.index) return;
@ -49,7 +59,7 @@ export function DashboardEdit({ websites }) {
} }
return ( return (
<> <Page loading={isLoading} error={error}>
<div className={styles.buttons}> <div className={styles.buttons}>
<Button onClick={handleSave} variant="action" size="small"> <Button onClick={handleSave} variant="action" size="small">
{formatMessage(labels.save)} {formatMessage(labels.save)}
@ -95,7 +105,7 @@ export function DashboardEdit({ websites }) {
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
</div> </div>
</> </Page>
); );
} }

View File

@ -1,211 +1,211 @@
{ {
"label.access-code": "Access code", "label.access-code": "Koda za dostop",
"label.actions": "Dejanja", "label.actions": "Dejanja",
"label.activity-log": "Activity log", "label.activity-log": "Dnevnik dejavnosti",
"label.add": "Add", "label.add": "Dodaj",
"label.add-description": "Add description", "label.add-description": "Dodaj opis",
"label.add-website": "Dodaj spletno mesto", "label.add-website": "Dodaj spletno mesto",
"label.admin": "Administrator", "label.admin": "Administrator",
"label.after": "After", "label.after": "Po",
"label.all": "Vse", "label.all": "Vsi",
"label.all-time": "All time", "label.all-time": "Ves čas",
"label.analytics": "Analytics", "label.analytics": "Analitika",
"label.average": "Average", "label.average": "Povprečno",
"label.average-visit-time": "Povprečni čas obiska", "label.average-visit-time": "Povprečni čas obiska",
"label.back": "Nazaj", "label.back": "Nazaj",
"label.before": "Before", "label.before": "Pred",
"label.bounce-rate": "Zapustna stopnja", "label.bounce-rate": "Odbojna stopnja",
"label.breakdown": "Breakdown", "label.breakdown": "Razčlenitev",
"label.browser": "Browser", "label.browser": "Brskalnik",
"label.browsers": "Brskalniki", "label.browsers": "Brskalniki",
"label.cancel": "Prekliči", "label.cancel": "Prekliči",
"label.change-password": "Zamenjaj geslo", "label.change-password": "Zamenjaj geslo",
"label.cities": "Cities", "label.cities": "Mesta",
"label.city": "City", "label.city": "Mesto",
"label.clear-all": "Clear all", "label.clear-all": "Počisti vse",
"label.confirm": "Confirm", "label.confirm": "Potrdi",
"label.confirm-password": "Potrditev gesla", "label.confirm-password": "Potrdi geslo",
"label.contains": "Contains", "label.contains": "Vsebuje",
"label.continue": "Continue", "label.continue": "Nadaljuj",
"label.countries": "Države", "label.countries": "Države",
"label.country": "Country", "label.country": "Država",
"label.create-report": "Create report", "label.create-report": "Ustvari poročilo",
"label.create-team": "Create team", "label.create-team": "Ustvari ekipo",
"label.create-user": "Create user", "label.create-user": "Ustvari uporabnika",
"label.created": "Created", "label.created": "Ustvarjeno",
"label.current-password": "Trenutno geslo", "label.current-password": "Trenutno geslo",
"label.custom-range": "Razpon po meri", "label.custom-range": "Obdobje po meri",
"label.dashboard": "Nadzorna plošča", "label.dashboard": "Nadzorna plošča",
"label.data": "Data", "label.data": "Podatki",
"label.date": "Date", "label.date": "Datum",
"label.date-range": "Časovni razpon", "label.date-range": "Časovno obdobje",
"label.day": "Day", "label.day": "Dan",
"label.default-date-range": "Privzeti časovni razpon", "label.default-date-range": "Privzeto časovno obdobje",
"label.delete": "Izbriši", "label.delete": "Izbriši",
"label.delete-team": "Delete team", "label.delete-team": "Izbriši ekipo",
"label.delete-user": "Delete user", "label.delete-user": "Izbriši uporabnika",
"label.delete-website": "Izbriši spletno mesto", "label.delete-website": "Izbriši spletno mesto",
"label.description": "Description", "label.description": "Opis",
"label.desktop": "Namizni računalnik", "label.desktop": "Namizni računalnik",
"label.details": "Details", "label.details": "Podrobnosti",
"label.device": "Device", "label.device": "Naprava",
"label.devices": "Naprave", "label.devices": "Naprave",
"label.dismiss": "Opusti", "label.dismiss": "Prezri",
"label.does-not-contain": "Does not contain", "label.does-not-contain": "Ne vsebuje",
"label.domain": "Domena", "label.domain": "Domena",
"label.dropoff": "Dropoff", "label.dropoff": "Zapustitev",
"label.edit": "Uredi", "label.edit": "Uredi",
"label.edit-dashboard": "Edit dashboard", "label.edit-dashboard": "Uredi nadzorno ploščo",
"label.enable-share-url": "Omogoči URL za skupno rabo", "label.enable-share-url": "Uredi povezavo za deljenje",
"label.event": "Event", "label.event": "Dogodek",
"label.event-data": "Event data", "label.event-data": "Podatki dogodka",
"label.events": "Dogodki", "label.events": "Dogodki",
"label.false": "False", "label.false": "Napačno",
"label.field": "Field", "label.field": "Polje",
"label.fields": "Fields", "label.fields": "Polja",
"label.filter-combined": "Skupno", "label.filter-combined": "Skupaj",
"label.filter-raw": "Neobdelane meritve", "label.filter-raw": "Neobdelano",
"label.filters": "Filters", "label.filters": "Filtri",
"label.funnel": "Funnel", "label.funnel": "Prodajni lijak",
"label.greater-than": "Greater than", "label.greater-than": "Večje od",
"label.greater-than-equals": "Greater than or equals", "label.greater-than-equals": "Večje ali enako kot",
"label.insights": "Insights", "label.insights": "Vpogled",
"label.is": "Is", "label.is": "Je",
"label.is-not": "Is not", "label.is-not": "Ni",
"label.is-not-set": "Is not set", "label.is-not-set": "Ni nastavljeno",
"label.is-set": "Is set", "label.is-set": "Je nastavljeno",
"label.join": "Join", "label.join": "Pridruži se",
"label.join-team": "Join team", "label.join-team": "Pridruži se ekipi",
"label.language": "Language", "label.language": "Jezik",
"label.languages": "Languages", "label.languages": "Jeziki",
"label.laptop": "Prenosni računalnik", "label.laptop": "Prenosni računalnik",
"label.last-days": "Zadnjih {x} dni", "label.last-days": "Zadnjih {x} dni",
"label.last-hours": "Zadnjih {x} ur", "label.last-hours": "Zadnjih {x} ur",
"label.leave": "Leave", "label.leave": "Zapusti",
"label.leave-team": "Leave team", "label.leave-team": "Zapusti ekipo",
"label.less-than": "Less than", "label.less-than": "Manjše kot",
"label.less-than-equals": "Less than or equals", "label.less-than-equals": "Manjše ali enako kot",
"label.login": "Prijava", "label.login": "Prijava",
"label.logout": "Odjava", "label.logout": "Odjava",
"label.max": "Max", "label.max": "Največ",
"label.members": "Members", "label.members": "Člani",
"label.min": "Min", "label.min": "Najmanj",
"label.mobile": "Mobilni telefon", "label.mobile": "Mobilne naprave",
"label.more": "Več", "label.more": "Več",
"label.my-websites": "My websites", "label.my-websites": "Moja spletna mesta",
"label.name": "Ime", "label.name": "Ime",
"label.new-password": "Novo geslo", "label.new-password": "Novo geslo",
"label.none": "None", "label.none": "Brez",
"label.os": "OS", "label.os": "OS",
"label.overview": "Overview", "label.overview": "Pregled",
"label.owner": "Owner", "label.owner": "Lastnik",
"label.page-of": "Page {current} of {total}", "label.page-of": "Stran {current} od {total}",
"label.page-views": "Ogledi strani", "label.page-views": "Obiski strani",
"label.pageTitle": "Page title", "label.pageTitle": "Naslov strani",
"label.pages": "Strani", "label.pages": "Strani",
"label.password": "Geslo", "label.password": "Geslo",
"label.powered-by": "Zagotavlja {name}", "label.powered-by": "Poganja {name}",
"label.profile": "Profil", "label.profile": "Profil",
"label.queries": "Queries", "label.queries": "Poizvedbe",
"label.query": "Query", "label.query": "Poizvedba",
"label.query-parameters": "Query parameters", "label.query-parameters": "Parametri poizvedbe",
"label.realtime": "V realnem času", "label.realtime": "V živo",
"label.referrer": "Referrer", "label.referrer": "Vir",
"label.referrers": "Viri", "label.referrers": "Viri",
"label.refresh": "Osveži", "label.refresh": "Osveži",
"label.regenerate": "Regenerate", "label.regenerate": "Ponovno generiraj",
"label.region": "Region", "label.region": "Regija",
"label.regions": "Regions", "label.regions": "Regije",
"label.remove": "Remove", "label.remove": "Odstrani",
"label.reports": "Reports", "label.reports": "Poročila",
"label.required": "Zahtevano", "label.required": "Zahtevano",
"label.reset": "Ponastavi", "label.reset": "Ponastavi",
"label.reset-website": "Reset statistics", "label.reset-website": "Ponastavi statistiko",
"label.retention": "Retention", "label.retention": "Ohranjanje uporabnikov",
"label.role": "Role", "label.role": "Vloga",
"label.run-query": "Run query", "label.run-query": "Izvedi poizvedbo",
"label.save": "Shrani", "label.save": "Shrani",
"label.screens": "Screens", "label.screens": "Zasloni",
"label.select-date": "Select date", "label.select-date": "Izberi datum",
"label.select-website": "Select website", "label.select-website": "Izberi spletno mesto",
"label.sessions": "Sessions", "label.sessions": "Seje",
"label.settings": "Nastavitve", "label.settings": "Nastavitve",
"label.share-url": "Deli URL", "label.share-url": "Deli povezavo",
"label.single-day": "En dan", "label.single-day": "En dan",
"label.sum": "Sum", "label.sum": "Seštevek",
"label.tablet": "Tablični računalnik", "label.tablet": "Tablični računalnik",
"label.team": "Team", "label.team": "Ekipa",
"label.team-guest": "Team guest", "label.team-guest": "Gost ekipe",
"label.team-id": "Team ID", "label.team-id": "ID ekipe",
"label.team-member": "Team member", "label.team-member": "Član ekipe",
"label.team-name": "Team name", "label.team-name": "Ime ekipe",
"label.team-owner": "Team owner", "label.team-owner": "Lastnik ekipe",
"label.team-websites": "Team websites", "label.team-websites": "Spletna mesta ekipe",
"label.teams": "Teams", "label.teams": "Ekipe",
"label.theme": "Theme", "label.theme": "Tema",
"label.this-month": "Ta mesec", "label.this-month": "Ta mesec",
"label.this-week": "Ta teden", "label.this-week": "Ta teden",
"label.this-year": "Letos", "label.this-year": "To leto",
"label.timezone": "Časovni pas", "label.timezone": "Časovni pas",
"label.title": "Title", "label.title": "Naslov",
"label.today": "Danes", "label.today": "Danes",
"label.toggle-charts": "Toggle charts", "label.toggle-charts": "Preklopi grafe",
"label.total": "Total", "label.total": "Skupaj",
"label.total-records": "Total records", "label.total-records": "Skupni zapisi",
"label.tracking-code": "Koda za sledenje", "label.tracking-code": "Koda za sledenje",
"label.true": "True", "label.true": "Pravilno",
"label.type": "Type", "label.type": "Vrsta",
"label.unique": "Unique", "label.unique": "Unikatni",
"label.unique-visitors": "Unikatni obiskovalci", "label.unique-visitors": "Unikatni obiskovalci",
"label.unknown": "Neznano", "label.unknown": "Neznano",
"label.untitled": "Untitled", "label.untitled": "Brez naslova",
"label.url": "URL", "label.url": "Povezava",
"label.urls": "URLs", "label.urls": "Povezave",
"label.user": "User", "label.user": "Uporabnik",
"label.username": "Uporabniško ime", "label.username": "Uporabniško ime",
"label.users": "Users", "label.users": "Uporabniki",
"label.value": "Value", "label.value": "Vrednost",
"label.view": "View", "label.view": "Poglej",
"label.view-details": "Prikaži podrobnosti", "label.view-details": "Poglej podrobnosti",
"label.view-only": "View only", "label.view-only": "Samo ogledovanje",
"label.views": "Ogledi", "label.views": "Obiski",
"label.visitors": "Obiskovalci", "label.visitors": "Obiskovalci",
"label.website": "Website", "label.website": "Spletno mesto",
"label.website-id": "Website ID", "label.website-id": "ID spletnega mesta",
"label.websites": "Spletna mesta", "label.websites": "Spletnih mest",
"label.window": "Window", "label.window": "Okno",
"label.yesterday": "Yesterday", "label.yesterday": "Včeraj",
"message.active-users": "{x} trenutni {x, plural, one {obiskovalec} other {obiskovalcev}}", "message.active-users": "{x} trenutni {x, plural, one {obiskovalec} other {obiskovalcev}}",
"message.confirm-delete": "Ste prepričani, da želite izbrisati {target}?", "message.confirm-delete": "Ste prepričani, da želite izbrisati {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-leave": "Ste prepričani, da želite zapustiti {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", "message.confirm-reset": "Ste prepričani, da želite ponastaviti statistiko {target}?",
"message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-account": "Za potrditev izbrisa tega računa vnesite {confirmation} v spodnje polje.",
"message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", "message.delete-website": "Za potrditev izbrisa tega spletnega mesta vnesite {confirmation} v spodnje polje.",
"message.delete-website-warning": "Izbrisani bodo tudi vsi povezani podatki.", "message.delete-website-warning": "Izbrisani bodo tudi vsi pripadajoči podatki.",
"message.error": "Prišlo je do napake.", "message.error": "Nekaj je šlo narobe.",
"message.event-log": "{event} on {url}", "message.event-log": "{event} na {url}",
"message.go-to-settings": "Pojdi v nastavitve", "message.go-to-settings": "Pojdi v nastavitve",
"message.incorrect-username-password": "Nepravilno uporabniško ime/geslo", "message.incorrect-username-password": "Nepravilno uporabniško ime/geslo.",
"message.invalid-domain": "Neveljavna domena", "message.invalid-domain": "Neveljavna domena",
"message.min-password-length": "Minimum length of {n} characters", "message.min-password-length": "Najmanjša dolžina je {n} znakov",
"message.new-version-available": "A new version of Umami {version} is available!", "message.new-version-available": "Na voljo je nova verzija programa Umami {version}!",
"message.no-data-available": "Podatki niso na voljo.", "message.no-data-available": "Podatki niso na voljo.",
"message.no-event-data": "No event data is available.", "message.no-event-data": "Podatki o dogodku niso na voljo.",
"message.no-match-password": "Gesli se ne ujemata", "message.no-match-password": "Gesli se ne ujemata",
"message.no-results-found": "No results were found.", "message.no-results-found": "Rezultatov ni bilo mogoče najti.",
"message.no-team-websites": "This team does not have any websites.", "message.no-team-websites": "Ta ekipa nima spletnih mest.",
"message.no-teams": "You have not created any teams.", "message.no-teams": "Niste še ustvarili nobene ekipe.",
"message.no-users": "There are no users.", "message.no-users": "Ni uporabnikov.",
"message.no-websites-configured": "Ni nastavljenih spletnih mest.", "message.no-websites-configured": "Nimate nastavljenih nobenih spletnih mest.",
"message.page-not-found": "Stran ni bila najdena.", "message.page-not-found": "Stran ni bila najdena.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", "message.reset-website": "Za ponastavitev izbrisa tega spletnega mesta vnesite {confirmation} v spodnje polje.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", "message.reset-website-warning": "Vse statistike za to spletno mesto bodo izbrisane, koda za sledenje pa bo ostala nespremenjena.",
"message.saved": "Uspešno shranjeno.", "message.saved": "Uspešno shranjeno.",
"message.share-url": "To je javno dostopen naslov URL za {target}.", "message.share-url": "To je javno dostopna povezava za {target}.",
"message.team-already-member": "You are already a member of the team.", "message.team-already-member": "Ste že član ekipe.",
"message.team-not-found": "Team not found.", "message.team-not-found": "Ekipa ni bila najdena.",
"message.team-websites-info": "Websites can be viewed by anyone on the team.", "message.team-websites-info": "Spletne strani si lahko ogleda vsak član ekipe.",
"message.tracking-code": "Koda za sledenje", "message.tracking-code": "Koda za sledenje",
"message.user-deleted": "User deleted.", "message.user-deleted": "Uporabnik je izbrisan.",
"message.visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}" "message.visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}"
} }

View File

@ -16,19 +16,19 @@
"label.before": "之前", "label.before": "之前",
"label.bounce-rate": "跳出率", "label.bounce-rate": "跳出率",
"label.breakdown": "故障", "label.breakdown": "故障",
"label.browser": "Browser", "label.browser": "浏览器",
"label.browsers": "浏览器", "label.browsers": "浏览器",
"label.cancel": "取消", "label.cancel": "取消",
"label.change-password": "更新密码", "label.change-password": "更新密码",
"label.cities": "市/县", "label.cities": "市/县",
"label.city": "City", "label.city": "市/县",
"label.clear-all": "清除全部", "label.clear-all": "清除全部",
"label.confirm": "确认", "label.confirm": "确认",
"label.confirm-password": "确认密码", "label.confirm-password": "确认密码",
"label.contains": "包含", "label.contains": "包含",
"label.continue": "继续", "label.continue": "继续",
"label.countries": "国家/地区", "label.countries": "国家/地区",
"label.country": "Country", "label.country": "国家/地区",
"label.create-report": "创建报告", "label.create-report": "创建报告",
"label.create-team": "创建团队", "label.create-team": "创建团队",
"label.create-user": "创建用户", "label.create-user": "创建用户",
@ -37,9 +37,9 @@
"label.custom-range": "自定义时间段", "label.custom-range": "自定义时间段",
"label.dashboard": "仪表板", "label.dashboard": "仪表板",
"label.data": "统计数据", "label.data": "统计数据",
"label.date": "Date", "label.date": "日期",
"label.date-range": "时间段", "label.date-range": "时间段",
"label.day": "Day", "label.day": "",
"label.default-date-range": "默认时间段", "label.default-date-range": "默认时间段",
"label.delete": "删除", "label.delete": "删除",
"label.delete-team": "删除团队", "label.delete-team": "删除团队",
@ -48,7 +48,7 @@
"label.description": "描述", "label.description": "描述",
"label.desktop": "台式机", "label.desktop": "台式机",
"label.details": "详细信息", "label.details": "详细信息",
"label.device": "Device", "label.device": "设备",
"label.devices": "设备", "label.devices": "设备",
"label.dismiss": "关闭", "label.dismiss": "关闭",
"label.does-not-contain": "不包含", "label.does-not-contain": "不包含",
@ -72,8 +72,8 @@
"label.insights": "见解", "label.insights": "见解",
"label.is": "等于", "label.is": "等于",
"label.is-not": "不等于", "label.is-not": "不等于",
"label.is-not-set": "Is not set", "label.is-not-set": "未设置",
"label.is-set": "Is set", "label.is-set": "已设置",
"label.join": "加入", "label.join": "加入",
"label.join-team": "加入团队", "label.join-team": "加入团队",
"label.language": "语言", "label.language": "语言",
@ -92,16 +92,16 @@
"label.min": "最小", "label.min": "最小",
"label.mobile": "手机", "label.mobile": "手机",
"label.more": "更多", "label.more": "更多",
"label.my-websites": "My websites", "label.my-websites": "我的网站",
"label.name": "名字", "label.name": "名字",
"label.new-password": "新密码", "label.new-password": "新密码",
"label.none": "无", "label.none": "无",
"label.os": "OS", "label.os": "OS",
"label.overview": "概览", "label.overview": "概览",
"label.owner": "所有者", "label.owner": "所有者",
"label.page-of": "Page {current} of {total}", "label.page-of": "总{total}中的第{current}页",
"label.page-views": "页面浏览量", "label.page-views": "页面浏览量",
"label.pageTitle": "Page title", "label.pageTitle": "标题",
"label.pages": "网页", "label.pages": "网页",
"label.password": "密码", "label.password": "密码",
"label.powered-by": "由 {name} 提供支持", "label.powered-by": "由 {name} 提供支持",
@ -110,18 +110,18 @@
"label.query": "查询", "label.query": "查询",
"label.query-parameters": "查询参数", "label.query-parameters": "查询参数",
"label.realtime": "实时", "label.realtime": "实时",
"label.referrer": "Referrer", "label.referrer": "来源",
"label.referrers": "来源域名", "label.referrers": "来源域名",
"label.refresh": "刷新", "label.refresh": "刷新",
"label.regenerate": "重新生成", "label.regenerate": "重新生成",
"label.region": "Region", "label.region": "州/省",
"label.regions": "州/省", "label.regions": "州/省",
"label.remove": "移除", "label.remove": "移除",
"label.reports": "报告", "label.reports": "报告",
"label.required": "必填", "label.required": "必填",
"label.reset": "重置", "label.reset": "重置",
"label.reset-website": "重置统计数据", "label.reset-website": "重置统计数据",
"label.retention": "Retention", "label.retention": "保留",
"label.role": "角色", "label.role": "角色",
"label.run-query": "查询", "label.run-query": "查询",
"label.save": "保存", "label.save": "保存",
@ -138,9 +138,9 @@
"label.team-guest": "团队访客", "label.team-guest": "团队访客",
"label.team-id": "团队 ID", "label.team-id": "团队 ID",
"label.team-member": "团队成员", "label.team-member": "团队成员",
"label.team-name": "Team name", "label.team-name": "团队名称",
"label.team-owner": "团队所有者", "label.team-owner": "团队所有者",
"label.team-websites": "Team websites", "label.team-websites": "团队网站",
"label.teams": "团队", "label.teams": "团队",
"label.theme": "主题", "label.theme": "主题",
"label.this-month": "本月", "label.this-month": "本月",
@ -155,19 +155,19 @@
"label.tracking-code": "跟踪代码", "label.tracking-code": "跟踪代码",
"label.true": "是", "label.true": "是",
"label.type": "类型", "label.type": "类型",
"label.unique": "Unique", "label.unique": "独立",
"label.unique-visitors": "独立访客", "label.unique-visitors": "独立访客",
"label.unknown": "未知", "label.unknown": "未知",
"label.untitled": "未命名", "label.untitled": "未命名",
"label.url": "URL", "label.url": "网址",
"label.urls": "URLs", "label.urls": "网址",
"label.user": "用户", "label.user": "用户",
"label.username": "用户名", "label.username": "用户名",
"label.users": "用户", "label.users": "用户",
"label.value": "值", "label.value": "值",
"label.view": "查看", "label.view": "查看",
"label.view-details": "查看更多", "label.view-details": "查看更多",
"label.view-only": "View only", "label.view-only": "仅浏览量",
"label.views": "浏览量", "label.views": "浏览量",
"label.visitors": "访客", "label.visitors": "访客",
"label.website": "网站", "label.website": "网站",
@ -183,7 +183,7 @@
"message.delete-website": "确定删除该网站, 请在下面的输入框中输入 {confirmation} 进行二次确认。", "message.delete-website": "确定删除该网站, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
"message.delete-website-warning": "所有相关数据将会被删除。", "message.delete-website-warning": "所有相关数据将会被删除。",
"message.error": "出现错误。", "message.error": "出现错误。",
"message.event-log": "{event} on {url}", "message.event-log": "{url}上的{event}",
"message.go-to-settings": "去设置", "message.go-to-settings": "去设置",
"message.incorrect-username-password": "用户名或密码不正确。", "message.incorrect-username-password": "用户名或密码不正确。",
"message.invalid-domain": "无效域名", "message.invalid-domain": "无效域名",

View File

@ -57,6 +57,14 @@ export function getDevice(screen, os) {
} }
} }
function getRegionCode(country, region) {
if (!country || !region) {
return undefined;
}
return region.includes('-') ? region : `${country}-${region}`;
}
export async function getLocation(ip, req) { export async function getLocation(ip, req) {
// Ignore local ips // Ignore local ips
if (await isLocalhost(ip)) { if (await isLocalhost(ip)) {
@ -71,7 +79,7 @@ export async function getLocation(ip, req) {
return { return {
country, country,
subdivision1: subdivision1.includes('-') ? subdivision1 : `${country}-${subdivision1}`, subdivision1: getRegionCode(country, subdivision1),
city, city,
}; };
} }
@ -84,7 +92,7 @@ export async function getLocation(ip, req) {
return { return {
country, country,
subdivision1: subdivision1.includes('-') ? subdivision1 : `${country}-${subdivision1}`, subdivision1: getRegionCode(country, subdivision1),
city, city,
}; };
} }
@ -122,11 +130,3 @@ export async function getClientInfo(req: NextApiRequestCollect, { screen }) {
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device }; return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
} }
export function getJsonBody<T>(req): T {
if ((req.headers['content-type'] || '').indexOf('text/plain') !== -1) {
return JSON.parse(req.body);
}
return req.body;
}

View File

@ -1,7 +1,7 @@
import { isUuid, secret, uuid } from 'lib/crypto'; import { isUuid, secret, uuid } from 'lib/crypto';
import { getClientInfo, getJsonBody } from 'lib/detect'; import { getClientInfo } from 'lib/detect';
import { parseToken } from 'next-basics'; import { parseToken } from 'next-basics';
import { CollectRequestBody, NextApiRequestCollect } from 'pages/api/send'; import { NextApiRequestCollect } from 'pages/api/send';
import { createSession } from 'queries'; import { createSession } from 'queries';
import cache from './cache'; import cache from './cache';
import clickhouse from './clickhouse'; import clickhouse from './clickhouse';
@ -22,7 +22,7 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
city: any; city: any;
ownerId: string; ownerId: string;
}> { }> {
const { payload } = getJsonBody<CollectRequestBody>(req); const { payload } = req.body;
if (!payload) { if (!payload) {
throw new Error('Invalid payload.'); throw new Error('Invalid payload.');

View File

@ -23,7 +23,6 @@ export interface UserPasswordRequestBody {
const schema = { const schema = {
POST: yup.object().shape({ POST: yup.object().shape({
id: yup.string().uuid().required(),
currentPassword: yup.string().required(), currentPassword: yup.string().required(),
newPassword: yup.string().min(8).required(), newPassword: yup.string().min(8).required(),
}), }),

View File

@ -3,11 +3,11 @@ import ipaddr from 'ipaddr.js';
import isbot from 'isbot'; import isbot from 'isbot';
import { COLLECTION_TYPE, HOSTNAME_REGEX } from 'lib/constants'; import { COLLECTION_TYPE, HOSTNAME_REGEX } from 'lib/constants';
import { secret } from 'lib/crypto'; import { secret } from 'lib/crypto';
import { getIpAddress, getJsonBody } from 'lib/detect'; import { getIpAddress } from 'lib/detect';
import { useCors, useSession, useValidate } from 'lib/middleware'; import { useCors, useSession, useValidate } from 'lib/middleware';
import { CollectionType, YupRequest } from 'lib/types'; import { CollectionType, YupRequest } from 'lib/types';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { badRequest, createToken, forbidden, ok, send } from 'next-basics'; import { badRequest, createToken, forbidden, methodNotAllowed, ok, send } from 'next-basics';
import { saveEvent, saveSessionData } from 'queries'; import { saveEvent, saveSessionData } from 'queries';
import * as yup from 'yup'; import * as yup from 'yup';
@ -73,71 +73,75 @@ const schema = {
export default async (req: NextApiRequestCollect, res: NextApiResponse) => { export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
await useCors(req, res); await useCors(req, res);
if (isbot(req.headers['user-agent']) && !process.env.DISABLE_BOT_CHECK) { if (req.method === 'POST') {
return ok(res); if (isbot(req.headers['user-agent']) && !process.env.DISABLE_BOT_CHECK) {
} return ok(res);
const { type, payload } = getJsonBody<CollectRequestBody>(req);
req.yup = schema;
await useValidate(req, res);
if (await hasBlockedIp(req)) {
return forbidden(res);
}
const { url, referrer, name: eventName, data: eventData, title: pageTitle } = payload;
await useSession(req, res);
const session = req.session;
if (type === COLLECTION_TYPE.event) {
// eslint-disable-next-line prefer-const
let [urlPath, urlQuery] = url?.split('?') || [];
let [referrerPath, referrerQuery] = referrer?.split('?') || [];
let referrerDomain;
if (!urlPath) {
urlPath = '/';
} }
if (referrerPath?.startsWith('http')) { const { type, payload } = req.body;
const refUrl = new URL(referrer);
referrerPath = refUrl.pathname; req.yup = schema;
referrerQuery = refUrl.search.substring(1); await useValidate(req, res);
referrerDomain = refUrl.hostname.replace(/www\./, '');
if (await hasBlockedIp(req)) {
return forbidden(res);
} }
if (process.env.REMOVE_TRAILING_SLASH) { const { url, referrer, name: eventName, data: eventData, title: pageTitle } = payload;
urlPath = urlPath.replace(/.+\/$/, '');
await useSession(req, res);
const session = req.session;
if (type === COLLECTION_TYPE.event) {
// eslint-disable-next-line prefer-const
let [urlPath, urlQuery] = url?.split('?') || [];
let [referrerPath, referrerQuery] = referrer?.split('?') || [];
let referrerDomain;
if (!urlPath) {
urlPath = '/';
}
if (referrerPath?.startsWith('http')) {
const refUrl = new URL(referrer);
referrerPath = refUrl.pathname;
referrerQuery = refUrl.search.substring(1);
referrerDomain = refUrl.hostname.replace(/www\./, '');
}
if (process.env.REMOVE_TRAILING_SLASH) {
urlPath = urlPath.replace(/.+\/$/, '');
}
await saveEvent({
urlPath,
urlQuery,
referrerPath,
referrerQuery,
referrerDomain,
pageTitle,
eventName,
eventData,
...session,
sessionId: session.id,
});
} }
await saveEvent({ if (type === COLLECTION_TYPE.identify) {
urlPath, if (!eventData) {
urlQuery, return badRequest(res, 'Data required.');
referrerPath, }
referrerQuery,
referrerDomain, await saveSessionData({ ...session, sessionData: eventData, sessionId: session.id });
pageTitle, }
eventName,
eventData, const token = createToken(session, secret());
...session,
sessionId: session.id, return send(res, token);
});
} }
if (type === COLLECTION_TYPE.identify) { return methodNotAllowed(res);
if (!eventData) {
return badRequest(res, 'Data required.');
}
await saveSessionData({ ...session, sessionData: eventData, sessionId: session.id });
}
const token = createToken(session, secret());
return send(res, token);
}; };
async function hasBlockedIp(req: NextApiRequestCollect) { async function hasBlockedIp(req: NextApiRequestCollect) {

View File

@ -42,8 +42,13 @@ export default async (
} = req.auth; } = req.auth;
if (req.method === 'GET') { if (req.method === 'GET') {
req.query.id = userId; if (!req.query.id) {
req.query.pageSize = 100; req.query.id = userId;
}
if (!req.query.pageSize) {
req.query.pageSize = 100;
}
return userWebsites(req as any, res); return userWebsites(req as any, res);
} }

View File

@ -142,6 +142,7 @@ export async function getReports(
...pageFilters, ...pageFilters,
...(options?.include && { include: options.include }), ...(options?.include && { include: options.include }),
}); });
const count = await prisma.client.report.count({ const count = await prisma.client.report.count({
where, where,
}); });

View File

@ -135,6 +135,7 @@ export async function getTeams(
...pageFilters, ...pageFilters,
...(options?.include && { include: options?.include }), ...(options?.include && { include: options?.include }),
}); });
const count = await prisma.client.team.count({ where }); const count = await prisma.client.team.count({ where });
return { data: teams, count, ...getParameters }; return { data: teams, count, ...getParameters };

View File

@ -107,7 +107,8 @@ export async function getWebsites(
...pageFilters, ...pageFilters,
...(options?.include && { include: options.include }), ...(options?.include && { include: options.include }),
}); });
const count = await prisma.client.website.count({ where });
const count = await prisma.client.website.count({ where: { ...where, deletedAt: null } });
return { data: websites, count, ...getParameters }; return { data: websites, count, ...getParameters };
} }