Merge branch 'master' into hosts-support

This commit is contained in:
Mike Cao 2024-06-18 23:01:09 -07:00 committed by GitHub
commit e11c2e452c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 3783 additions and 2197 deletions

View file

@ -40,6 +40,9 @@ export function TeamMemberEditForm({
};
const renderValue = (value: string) => {
if (value === ROLES.teamManager) {
return formatMessage(labels.manager);
}
if (value === ROLES.teamMember) {
return formatMessage(labels.member);
}
@ -58,6 +61,7 @@ export function TeamMemberEditForm({
minWidth: '250px',
}}
>
<Item key={ROLES.teamManager}>{formatMessage(labels.manager)}</Item>
<Item key={ROLES.teamMember}>{formatMessage(labels.member)}</Item>
<Item key={ROLES.teamViewOnly}>{formatMessage(labels.viewOnly)}</Item>
</Dropdown>

View file

@ -12,8 +12,10 @@ export function TeamMembersPage({ teamId }: { teamId: string }) {
const { formatMessage, labels } = useMessages();
const canEdit =
team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
user.role !== ROLES.viewOnly;
team?.teamUser?.find(
({ userId, role }) =>
(role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
) && user.role !== ROLES.viewOnly;
return (
<>

View file

@ -19,6 +19,7 @@ export function TeamMembersTable({
const roles = {
[ROLES.teamOwner]: formatMessage(labels.teamOwner),
[ROLES.teamManager]: formatMessage(labels.teamManager),
[ROLES.teamMember]: formatMessage(labels.teamMember),
[ROLES.teamViewOnly]: formatMessage(labels.viewOnly),
};

View file

@ -15,18 +15,24 @@ export function TeamDetails({ teamId }: { teamId: string }) {
const { user } = useLogin();
const [tab, setTab] = useState('details');
const canEdit =
const isTeamOwner =
!!team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
user.role !== ROLES.viewOnly;
const canEdit =
!!team?.teamUser?.find(
({ userId, role }) =>
(role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
) && user.role !== ROLES.viewOnly;
return (
<Flexbox direction="column">
<PageHeader title={team?.name} icon={<Icons.Users />}>
{!canEdit && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
{!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
</PageHeader>
<Tabs selectedKey={tab} onSelect={(value: any) => setTab(value)} style={{ marginBottom: 30 }}>
<Item key="details">{formatMessage(labels.details)}</Item>
{canEdit && <Item key="manage">{formatMessage(labels.manage)}</Item>}
{isTeamOwner && <Item key="manage">{formatMessage(labels.manage)}</Item>}
</Tabs>
{tab === 'details' && <TeamEditForm teamId={teamId} allowEdit={canEdit} />}
{tab === 'manage' && <TeamManage teamId={teamId} />}

View file

@ -0,0 +1,3 @@
import Page from 'app/(main)/reports/utm/page';
export default Page;

View file

@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/event-data/page';
export default Page;

View file

@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/realtime/page';
export default Page;

View file

@ -0,0 +1,3 @@
import Page from 'app/(main)/websites/[websiteId]/reports/page';
export default Page;

View file

@ -1,6 +1,6 @@
import classNames from 'classnames';
import Favicon from 'components/common/Favicon';
import { useMessages, useWebsite } from 'components/hooks';
import { useMessages, useTeamUrl, useWebsite } from 'components/hooks';
import Icons from 'components/icons';
import ActiveUsers from 'components/metrics/ActiveUsers';
import Link from 'next/link';
@ -19,6 +19,7 @@ export function WebsiteHeader({
children?: ReactNode;
}) {
const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useTeamUrl();
const pathname = usePathname();
const { data: website } = useWebsite(websiteId);
const { name, domain } = website || {};
@ -62,7 +63,11 @@ export function WebsiteHeader({
: pathname.match(/^\/websites\/[\w-]+$/);
return (
<Link key={label} href={`/websites/${websiteId}${path}`} shallow={true}>
<Link
key={label}
href={renderTeamUrl(`/websites/${websiteId}${path}`)}
shallow={true}
>
<Button
variant="quiet"
className={classNames({

View file

@ -1,18 +1,19 @@
'use client';
import Link from 'next/link';
import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages, useTeamUrl } from 'components/hooks';
import WebsiteHeader from '../WebsiteHeader';
import ReportsDataTable from 'app/(main)/reports/ReportsDataTable';
export function WebsiteReportsPage({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useTeamUrl();
return (
<>
<WebsiteHeader websiteId={websiteId} />
<Flexbox alignItems="center" justifyContent="end">
<Link href={`/reports/create`}>
<Link href={renderTeamUrl('/reports/create')}>
<Button variant="primary">
<Icon>
<Icons.Plus />

View file

@ -1,14 +1,14 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useState } from 'react';
import { useApi } from './useApi';
import { FilterResult, SearchFilter, FilterQueryResult } from 'lib/types';
import { PageResult, PageParams, FilterQueryResult } from 'lib/types';
export function useFilterQuery<T = any>({
queryKey,
queryFn,
...options
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): FilterQueryResult<T> {
const [params, setParams] = useState<T | SearchFilter>({
const [params, setParams] = useState<T | PageParams>({
query: '',
page: 1,
});
@ -21,7 +21,7 @@ export function useFilterQuery<T = any>({
});
return {
result: data as FilterResult<any>,
result: data as PageResult<any>,
query,
params,
setParams,

View file

@ -1,33 +1,17 @@
import useApi from './useApi';
import { useFilterParams } from '../useFilterParams';
import { UseQueryOptions } from '@tanstack/react-query';
import { useDateRange, useNavigation, useTimezone } from 'components/hooks';
import { zonedTimeToUtc } from 'date-fns-tz';
export function useWebsiteEvents(
websiteId: string,
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, offset } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, event },
} = useNavigation();
const params = {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
offset,
timezone,
url,
event,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['events', { ...params }],
queryFn: () => get(`/websites/${websiteId}/events`, { ...params }),
queryKey: ['websites:events', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/events`, params),
enabled: !!websiteId,
...options,
});

View file

@ -1,12 +1,15 @@
import useApi from './useApi';
import { UseQueryOptions } from '@tanstack/react-query';
import useApi from './useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteMetrics(
websiteId: string,
params?: { [key: string]: any },
type: string,
limit: number,
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const params = useFilterParams(websiteId);
return useQuery({
queryKey: [
@ -14,21 +17,26 @@ export function useWebsiteMetrics(
{
websiteId,
...params,
type,
limit,
},
],
queryFn: async () => {
const filters = { ...params };
filters[params.type] = undefined;
filters[type] = undefined;
const data = await get(`/websites/${websiteId}/metrics`, {
...filters,
type,
limit,
});
options?.onDataLoad?.(data);
return data;
},
enabled: !!websiteId,
...options,
});
}

View file

@ -1,35 +1,18 @@
import { zonedTimeToUtc } from 'date-fns-tz';
import { useApi, useDateRange, useNavigation, useTimezone } from 'components/hooks';
import { UseQueryOptions } from '@tanstack/react-query';
import { useApi } from './useApi';
import { useFilterParams } from '..//useFilterParams';
export function useWebsitePageviews(websiteId: string, options?: { [key: string]: string }) {
export function useWebsitePageviews(
websiteId: string,
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, referrer, host, os, browser, device, country, region, city, title },
} = useNavigation();
const params = {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
timezone,
url,
referrer,
host,
os,
browser,
device,
country,
region,
city,
title,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['websites:pageviews', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, params),
enabled: !!websiteId,
...options,
});
}

View file

@ -1,31 +1,14 @@
import { useApi, useDateRange, useNavigation } from 'components/hooks';
import { useApi } from './useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteStats(websiteId: string, options?: { [key: string]: string }) {
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
const {
query: { url, referrer, host, title, os, browser, device, country, region, city },
} = useNavigation();
const params = {
startAt: +startDate,
endAt: +endDate,
url,
referrer,
host,
title,
os,
browser,
device,
country,
region,
city,
};
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['websites:stats', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/stats`, params),
enabled: !!websiteId,
...options,
});
}

View file

@ -0,0 +1,32 @@
import { useNavigation } from './useNavigation';
import { useDateRange } from './useDateRange';
import { useTimezone } from './useTimezone';
import { zonedTimeToUtc } from 'date-fns-tz';
export function useFilterParams(websiteId: string) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, offset } = dateRange;
const { timezone } = useTimezone();
const {
query: { url, referrer, title, query, os, browser, device, country, region, city, event },
} = useNavigation();
return {
startAt: +zonedTimeToUtc(startDate, timezone),
endAt: +zonedTimeToUtc(endDate, timezone),
unit,
offset,
timezone,
url,
referrer,
title,
query,
os,
browser,
device,
country,
region,
city,
event,
};
}

View file

@ -29,6 +29,7 @@ export const labels = defineMessages({
createdBy: { id: 'label.created-by', defaultMessage: 'Created By' },
edit: { id: 'label.edit', defaultMessage: 'Edit' },
name: { id: 'label.name', defaultMessage: 'Name' },
manager: { id: 'label.manager', defaultMessage: 'Manager' },
member: { id: 'label.member', defaultMessage: 'Member' },
members: { id: 'label.members', defaultMessage: 'Members' },
accessCode: { id: 'label.access-code', defaultMessage: 'Access code' },
@ -43,6 +44,7 @@ export const labels = defineMessages({
settings: { id: 'label.settings', defaultMessage: 'Settings' },
owner: { id: 'label.owner', defaultMessage: 'Owner' },
teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' },
teamManager: { id: 'label.team-manager', defaultMessage: 'Team manager' },
teamMember: { id: 'label.team-member', defaultMessage: 'Team member' },
teamViewOnly: { id: 'label.team-view-only', defaultMessage: 'Team view only' },
enableShareUrl: { id: 'label.enable-share-url', defaultMessage: 'Enable share URL' },

View file

@ -6,7 +6,6 @@ import LinkButton from 'components/common/LinkButton';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { percentFilter } from 'lib/filters';
import {
useDateRange,
useNavigation,
useWebsiteMetrics,
useMessages,
@ -45,35 +44,14 @@ export function MetricsTable({
}: MetricsTableProps) {
const [search, setSearch] = useState('');
const { formatValue } = useFormat();
const [{ startDate, endDate }] = useDateRange(websiteId);
const {
renderUrl,
query: { url, referrer, host, title, os, browser, device, country, region, city },
} = useNavigation();
const { renderUrl } = useNavigation();
const { formatMessage, labels } = useMessages();
const { dir } = useLocale();
const { data, isLoading, isFetched, error } = useWebsiteMetrics(
websiteId,
{
type,
startAt: +startDate,
endAt: +endDate,
url,
referrer,
host,
os,
title,
browser,
device,
country,
region,
city,
limit,
search,
},
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
);
const { data, isLoading, isFetched, error } = useWebsiteMetrics(websiteId, type, limit, {
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
onDataLoad,
});
const filteredData = useMemo(() => {
if (data) {

View file

@ -38,7 +38,11 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp
id={view}
value={x}
label={!x && formatMessage(labels.none)}
externalUrl={`${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`}
externalUrl={
view === 'url'
? `${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`
: null
}
/>
);
};

246
src/lang/bs-BA.json Normal file
View file

@ -0,0 +1,246 @@
{
"label.access-code": "Pristupni kod",
"label.actions": "Akcije",
"label.activity-log": "Log aktivnosti",
"label.add": "Dodaj",
"label.add-description": "Dodaj opis",
"label.add-member": "Dodaj člana",
"label.add-step": "Dodaj korak",
"label.add-website": "Dodaj web stranicu",
"label.admin": "Administrator",
"label.after": "Nakon",
"label.all": "Sve",
"label.all-time": "Cijelo vrijeme",
"label.analytics": "Analitike",
"label.average": "Prosjek",
"label.average-visit-time": "Prosječno vrijeme posjete",
"label.back": "Nazad",
"label.before": "Prije",
"label.bounce-rate": "Bounce rate",
"label.breakdown": "Pregled po kategorijama",
"label.browser": "Browser",
"label.browsers": "Browseri",
"label.cancel": "Otkaži",
"label.change-password": "Promijeni šifru",
"label.cities": "Gradovi",
"label.city": "Grad",
"label.clear-all": "Očisti sve",
"label.confirm": "Potvrdi",
"label.confirm-password": "Potvrdi šifru",
"label.contains": "Sadrži",
"label.continue": "Nastavi",
"label.countries": "Zemlje",
"label.country": "Zemlja",
"label.create": "Kreiraj",
"label.create-report": "Kreiraj izvještaj",
"label.create-team": "Kreiraj tim",
"label.create-user": "Kreiraj korisnika",
"label.created": "Kreiraj",
"label.created-by": "Kreirao",
"label.current-password": "Trenutna šifra",
"label.custom-range": "Proizvoljni raspon",
"label.dashboard": "Dashboard",
"label.data": "Podaci",
"label.date": "Datum",
"label.date-range": "Datumski raspon",
"label.day": "Dan",
"label.default-date-range": "Defaultni datumski raspon",
"label.delete": "Izbriši",
"label.delete-report": "Izbriši report",
"label.delete-team": "Izbriši tim",
"label.delete-user": "Izbriši korisnika",
"label.delete-website": "Izbriši web stranicu",
"label.description": "Opis",
"label.desktop": "Desktop",
"label.details": "Detalji",
"label.device": "Uređaj",
"label.devices": "Uređaji",
"label.dismiss": "Odbaci",
"label.does-not-contain": "Ne sadrži",
"label.domain": "Domena",
"label.dropoff": "Dropoff",
"label.edit": "Uredi",
"label.edit-dashboard": "Uredi dashboard",
"label.edit-member": "Uredi člana",
"label.enable-share-url": "Omogući URL za dijeljenje",
"label.event": "Događaj",
"label.event-data": "Podaci o događaju",
"label.events": "Događaji",
"label.false": "Ne",
"label.field": "Polje",
"label.fields": "Polja",
"label.filter": "Filter",
"label.filter-combined": "Kombinovano",
"label.filter-raw": "Sirovo",
"label.filters": "Filtri",
"label.funnel": "Lijevak",
"label.funnel-description": "Razumite koverziju i drop-off učestalost korisnika.",
"label.greater-than": "Veće od",
"label.greater-than-equals": "Veće od ili jednako",
"label.insights": "Uvidi",
"label.insights-description": "Zaronite dublje u vaše podatke korištenjem segmenata i filtera",
"label.is": "Jeste",
"label.is-not": "Nije",
"label.is-not-set": "Nije setano",
"label.is-set": "Jeste setano",
"label.join": "Učlani se",
"label.join-team": "Učlani se u tim",
"label.language": "Jezik",
"label.languages": "Jezici",
"label.laptop": "Laptop",
"label.last-days": "Zadnjih {x} dana",
"label.last-hours": "Zadnjih {x} sati",
"label.last-months": "Zadnjih {x} mjeseci",
"label.leave": "Napusti",
"label.leave-team": "Napusti tim",
"label.less-than": "Manje od",
"label.less-than-equals": "Manje od ili jednako",
"label.login": "Login",
"label.logout": "Logout",
"label.manage": "Manage",
"label.max": "Max",
"label.member": "Član",
"label.members": "Članovi",
"label.min": "Min",
"label.mobile": "Mobile",
"label.more": "Više",
"label.my-account": "Moj račun",
"label.my-websites": "Moje web stranice",
"label.name": "Ime",
"label.new-password": "Nova šifra",
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.os": "OS",
"label.overview": "Pregled",
"label.owner": "Vlasnik",
"label.page-of": "Strana {current} od {total}",
"label.page-views": "Pregleda stranica",
"label.pageTitle": "Naslov stranice",
"label.pages": "Stranice",
"label.password": "Šifra",
"label.powered-by": "Omogućeno s {name}",
"label.profile": "Profil",
"label.queries": "Queryji",
"label.query": "Query",
"label.query-parameters": "Query parametri",
"label.realtime": "Realno vrijeme",
"label.referrer": "Referrer",
"label.referrers": "Referrers",
"label.refresh": "Refresh",
"label.regenerate": "Regeneriši",
"label.region": "Region",
"label.regions": "Regioni",
"label.remove": "Ukloni",
"label.remove-member": "Ukloni člana",
"label.reports": "Izvještaji",
"label.required": "Required",
"label.reset": "Resetuj",
"label.reset-website": "Resetuj web stranicu",
"label.retention": "Retention",
"label.retention-description": "Izmjeri 'ljepljivost' svoje web stranice praćenjem koliko često set korisnici vraćaju.",
"label.role": "Rola",
"label.run-query": "Pokreni query",
"label.save": "Sačuvaj",
"label.screens": "Ekrani",
"label.search": "Traži",
"label.select": "Odaberi",
"label.select-date": "Odaberi datum",
"label.select-role": "Odaberi rolu",
"label.select-website": "Odaberi web stranicu",
"label.sessions": "Sesije",
"label.settings": "Postavke",
"label.share-url": "Share URL",
"label.single-day": "Jedan dan",
"label.steps": "Koraci",
"label.sum": "Suma",
"label.tablet": "Tablet",
"label.team": "Tim",
"label.team-id": "Tim ID",
"label.team-member": "Član tima",
"label.team-name": "Naziv tima",
"label.team-owner": "Vlasnik tima",
"label.team-view-only": "Samo tim može vidjeti",
"label.team-websites": "Timske web stranice",
"label.teams": "Timovi",
"label.theme": "Teme",
"label.this-month": "Ovaj mjesec",
"label.this-week": "Ova sedmica",
"label.this-year": "Ova godina",
"label.timezone": "Vremenska zona",
"label.title": "Naslov",
"label.today": "Danas",
"label.toggle-charts": "Uklj/isklj grafikone",
"label.total": "Ukupno",
"label.total-records": "Ukupno redova",
"label.tracking-code": "Kod za praćenje",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer web stranice",
"label.true": "Da",
"label.type": "Tip",
"label.unique": "Jedinstveno",
"label.unique-visitors": "Jedinstvenih posjetitelja",
"label.unknown": "Nepoznato",
"label.untitled": "Bezimeno",
"label.update": "Update",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "Korisnik",
"label.username": "Korisničko ime",
"label.users": "Korisnici",
"label.utm": "UTM",
"label.utm-description": "Pratite vaše kampanje kroz UTM parametre.",
"label.value": "Vrijednost",
"label.view": "Pregled",
"label.view-details": "Pogledaj detalje",
"label.view-only": "Samo gledanje",
"label.views": "Pregledi",
"label.views-per-visit": "Pregledi po posjeti",
"label.visitors": "Posjetitelji",
"label.visits": "Posjete",
"label.website": "Web stranica",
"label.website-id": "ID web stranice",
"label.websites": "Web stranice",
"label.window": "Prozor",
"label.yesterday": "Jučer",
"message.action-confirmation": "Unesite {confirmation} ispod da potvrdite.",
"message.active-users": "{x} trenutno {x, plural, one {posjetitelj} other {posjetitelja}}",
"message.confirm-delete": "Jeste li sigurni da želite obrisati {target}?",
"message.confirm-leave": "Jeste li sigurni da želite napustiti {target}?",
"message.confirm-remove": "Jeste li sigurni da želite ukloniti {target}?",
"message.confirm-reset": "Jeste li sigurni da želite resetovati {target}?",
"message.delete-team-warning": "Brisanje tima će također obrisati sve web stranice tima.",
"message.delete-website-warning": "Svi podaci web stranice biće obrisani.",
"message.error": "Nešto je pošlo po zlu.",
"message.event-log": "{event} na {url}",
"message.go-to-settings": "Idi na postavke",
"message.incorrect-username-password": "Pogrešno korisničko ime i/ili šifra.",
"message.invalid-domain": "Nevalidna domena. Ne uključujte http/https.",
"message.min-password-length": "Minimalna dužina od {n} karaktera",
"message.new-version-available": "Nova verzija Umami {version} je dostupna!",
"message.no-data-available": "Nema dostupnih podataka.",
"message.no-event-data": "Nema dostupnih podataka o događajima.",
"message.no-match-password": "Šifre se ne poklapaju.",
"message.no-results-found": "Nema rezultata.",
"message.no-team-websites": "Ovaj tim nema nikakvih web stranica.",
"message.no-teams": "Niste kreirali nijedan tim.",
"message.no-users": "Nema nikakvih korisnika.",
"message.no-websites-configured": "Nemate iskonfigurisanu nijednu web stranicu.",
"message.page-not-found": "Stranica nije pronađena",
"message.reset-website": "Da resetujete ovu web stranicu, upišite {confirmation} dole da potvrdite.",
"message.reset-website-warning": "Sve statistike o ovoj web stranici će biti obrisane, ali vaše postavke neće biti dirane.",
"message.saved": "Sačuvano.",
"message.share-url": "Statistike vaše web stranice su javno dostupne na sljedećem URLu:",
"message.team-already-member": "Već ste član tima.",
"message.team-not-found": "Tim nije pronađen.",
"message.team-websites-info": "Web stranice može vidjeti bilo ko iz tima.",
"message.tracking-code": "Da pratite statistike ove web stranice, stavite sljedeći kod u <head>...</head> sekciju vašeg HTMLa.",
"message.transfer-team-website-to-user": "Prebacite ovu web stranicu na vaš račun?",
"message.transfer-user-website-to-team": "Odaberite tim u koji želite prebaciti ovu web stranicu.",
"message.transfer-website": "Prebacite vlasništvo web stranice na vaš račun ili drugi tim.",
"message.triggered-event": "Trigerovani događaj",
"message.user-deleted": "Korisnik obrisan.",
"message.viewed-page": "Pogledana stranica",
"message.visitor-log": "Posjetitelj iz {country} koristi {browser} na {os} {device}",
"message.visitors-dropped-off": "Posjetitelji koji su napustili stranicu"
}

View file

@ -1,246 +1,246 @@
{
"label.access-code": "Access code",
"label.access-code": "Codi d'accés",
"label.actions": "Accions",
"label.activity-log": "Activity log",
"label.add": "Add",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "Afegeix lloc web",
"label.activity-log": "Registre d'activitat",
"label.add": "Afegir",
"label.add-description": "Afegir descripció",
"label.add-member": "Afegir membre",
"label.add-step": "Afegir pas",
"label.add-website": "Afegir lloc web",
"label.admin": "Administrador",
"label.after": "After",
"label.after": "Després",
"label.all": "Tots",
"label.all-time": "Sempre",
"label.analytics": "Analytics",
"label.average": "Average",
"label.analytics": "Analítiques",
"label.average": "Mitjana",
"label.average-visit-time": "Temps mitjà de visita",
"label.back": "Enrere",
"label.before": "Before",
"label.before": "Abans",
"label.bounce-rate": "Percentatge de rebot",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.breakdown": "Desglossament",
"label.browser": "Navegador",
"label.browsers": "Navegadors",
"label.cancel": "Cancel·la",
"label.change-password": "Canvia la contrasenya",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.cities": "Ciutats",
"label.city": "Ciutat",
"label.clear-all": "Netejar tot",
"label.confirm": "Confirmar",
"label.confirm-password": "Confirma la contrasenya",
"label.contains": "Contains",
"label.continue": "Continue",
"label.contains": "Conté",
"label.continue": "Continuar",
"label.countries": "Països",
"label.country": "Country",
"label.create": "Create",
"label.create-report": "Create report",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
"label.country": "País",
"label.create": "Crear",
"label.create-report": "Crear informe",
"label.create-team": "Crear equip",
"label.create-user": "Crear usuari",
"label.created": "Creat",
"label.created-by": "Creat Per",
"label.current-password": "Contrasenya actual",
"label.custom-range": "Rang personalitzat",
"label.dashboard": "Panell",
"label.data": "Data",
"label.date": "Date",
"label.data": "Dades",
"label.date": "Data",
"label.date-range": "Interval de dates",
"label.day": "Day",
"label.day": "Dia",
"label.default-date-range": "Interval de dates per defecte",
"label.delete": "Esborra",
"label.delete-report": "Delete report",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-report": "Eliminar informe",
"label.delete-team": "Eliminar equip",
"label.delete-user": "Eliminar usuari",
"label.delete-website": "Esborra el lloc web",
"label.description": "Description",
"label.description": "Descripció",
"label.desktop": "Escriptori",
"label.details": "Details",
"label.device": "Device",
"label.details": "Detalls",
"label.device": "Dispositiu",
"label.devices": "Dispositius",
"label.dismiss": "Descarta",
"label.does-not-contain": "Does not contain",
"label.does-not-contain": "No conté",
"label.domain": "Domini",
"label.dropoff": "Dropoff",
"label.dropoff": "Abandonament",
"label.edit": "Edita",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
"label.edit-dashboard": "Edita panell",
"label.edit-member": "Edita membre",
"label.enable-share-url": "Activa l'enllaç per compartir",
"label.event": "Event",
"label.event-data": "Event data",
"label.event": "Esdeveniment",
"label.event-data": "Dades de l'esdeveniment",
"label.events": "Esdeveniments",
"label.false": "False",
"label.field": "Field",
"label.fields": "Fields",
"label.filter": "Filter",
"label.false": "Fals",
"label.field": "Camp",
"label.fields": "Camps",
"label.filter": "Filtre",
"label.filter-combined": "Combinat",
"label.filter-raw": "En cru",
"label.filters": "Filters",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
"label.filters": "Filtres",
"label.funnel": "Embut",
"label.funnel-description": "Entengui la taxa de conversió i abandonament dels usuaris.",
"label.greater-than": "Més gran que",
"label.greater-than-equals": "Més gran que o igual a",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Language",
"label.languages": "Llengües",
"label.insights-description": "Aprofundeixi en les seves dades mitjançant l'ús de segments i filtres.",
"label.is": "És igual a",
"label.is-not": "No és igual a",
"label.is-not-set": "No està establert",
"label.is-set": "Està establert",
"label.join": "Unir",
"label.join-team": "Unir-se al equip",
"label.language": "Idioma",
"label.languages": "Idiomes",
"label.laptop": "Portàtil",
"label.last-days": "Últims {x} dies",
"label.last-hours": "Últimes {x} hores",
"label.last-months": "Last {x} months",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
"label.last-months": "Últims {x} mesos",
"label.leave": "Abandonar",
"label.leave-team": "Abandonar equip",
"label.less-than": "Menor que",
"label.less-than-equals": "Menor que o igual a",
"label.login": "Connecta't",
"label.logout": "Desconnecta't",
"label.manage": "Manage",
"label.max": "Max",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.manage": "Administrar",
"label.max": "Màx",
"label.member": "Membre",
"label.members": "Membres",
"label.min": "Mín",
"label.mobile": "Mòbil",
"label.more": "Més",
"label.my-account": "My account",
"label.my-websites": "My websites",
"label.my-account": "El meu compte",
"label.my-websites": "Els meus llocs web",
"label.name": "Nom",
"label.new-password": "Contrasenya nova",
"label.none": "None",
"label.none": "Cap",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.os": "OS",
"label.overview": "Overview",
"label.os": "SO",
"label.overview": "Resum",
"label.owner": "Propietari",
"label.page-of": "Page {current} of {total}",
"label.page-of": "Pàgina {current} de {total}",
"label.page-views": "Pàgines vistes",
"label.pageTitle": "Page title",
"label.pageTitle": "Títol de la pàgina",
"label.pages": "Pàgines",
"label.password": "Contrasenya",
"label.powered-by": "Funciona amb {name}",
"label.profile": "Perfil",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.queries": "Consultes",
"label.query": "Consulta",
"label.query-parameters": "Paràmetres de consulta",
"label.realtime": "Temps real",
"label.referrer": "Referrer",
"label.referrer": "Referent",
"label.referrers": "Referents",
"label.refresh": "Refresca",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regenerate": "Regenerar",
"label.region": "Regió",
"label.regions": "Regions",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
"label.remove": "Treure",
"label.remove-member": "Eliminar membre",
"label.reports": "Informes",
"label.required": "Obligatori",
"label.reset": "Restableix",
"label.reset-website": "Restableix estadístiques",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.role": "Role",
"label.run-query": "Run query",
"label.retention": "Retenció",
"label.retention-description": "Mesuri la retenció del seu lloc web fent un seguiment de la freqüència amb què tornen els usuaris.",
"label.role": "Rol",
"label.run-query": "Executar consulta",
"label.save": "Desa",
"label.screens": "Screens",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.screens": "Pantalles",
"label.search": "Buscar",
"label.select": "Seleccionar",
"label.select-date": "Seleccionar data",
"label.select-role": "Seleccionar rol",
"label.select-website": "Seleccionar lloc web",
"label.sessions": "Sessions",
"label.settings": "Configuració",
"label.share-url": "Enllaç per compartir",
"label.single-day": "Un sol dia",
"label.steps": "Steps",
"label.sum": "Sum",
"label.steps": "Pasos",
"label.sum": "Suma",
"label.tablet": "Tauleta",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
"label.theme": "Theme",
"label.team": "Equip",
"label.team-id": "ID del equip",
"label.team-member": "Membre de l'equip",
"label.team-name": "Nom de l'equip",
"label.team-owner": "Propietari de l'equip",
"label.team-view-only": "Vista només de l'equip",
"label.team-websites": "Llocs web de l'equip",
"label.teams": "Equips",
"label.theme": "Tema",
"label.this-month": "Aquest mes",
"label.this-week": "Aquesta setmana",
"label.this-year": "Aquest any",
"label.timezone": "Zona horària",
"label.title": "Title",
"label.title": "Títol",
"label.today": "Avui",
"label.toggle-charts": "Mostra/amaga gràfics",
"label.total": "Total",
"label.total-records": "Total records",
"label.total-records": "Total de registres",
"label.tracking-code": "Codi de seguiment",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer website",
"label.true": "True",
"label.type": "Type",
"label.unique": "Unique",
"label.transfer": "Transferir",
"label.transfer-website": "Transferir lloc web",
"label.true": "Cert",
"label.type": "Tipus",
"label.unique": "Únic",
"label.unique-visitors": "Visitants únics",
"label.unknown": "Desconegut",
"label.untitled": "Untitled",
"label.update": "Update",
"label.untitled": "Sense títol",
"label.update": "Actualitzar",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "User",
"label.user": "Usuari",
"label.username": "Nom d'usuari",
"label.users": "Users",
"label.users": "Usuaris",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.value": "Value",
"label.view": "View",
"label.utm-description": "Rastreji les seves campanyes a través de paràmetres UTM.",
"label.value": "Valor",
"label.view": "Visualitzar",
"label.view-details": "Veure els detalls",
"label.view-only": "View only",
"label.view-only": "Només veure",
"label.views": "Vistes",
"label.views-per-visit": "Views per visit",
"label.visitors": "Visitants",
"label.visits": "Visits",
"label.website": "Website",
"label.website-id": "Website ID",
"label.visits": "Visites",
"label.website": "Lloc web",
"label.website-id": "ID del lloc web",
"label.websites": "Llocs web",
"label.window": "Window",
"label.window": "Finestra",
"label.yesterday": "Ahir",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.action-confirmation": "Escrigui {confirmation} al cuadre inferior per confirmar.",
"message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}",
"message.confirm-delete": "Segur que vols esborrar {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-remove": "Are you sure you want to remove {target}?",
"message.confirm-reset": "Segur que vols restablir les estadístiques de {target}?",
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
"message.confirm-delete": "Segur que vol esborrar {target}?",
"message.confirm-leave": "Segur que vol abandonar {target}?",
"message.confirm-remove": "Segur que vol eliminar {target}?",
"message.confirm-reset": "Segur que vol restablir les estadístiques de {target}?",
"message.delete-team-warning": "Al eliminar un equip també s'eliminaran tots els llocs web de l'equip.",
"message.delete-website-warning": "També s'esborraran totes les dades relacionades.",
"message.error": "S'ha produït un error.",
"message.event-log": "{event} on {url}",
"message.event-log": "{event} a {url}",
"message.go-to-settings": "Vés a la configuració",
"message.incorrect-username-password": "Nom d'usuari o contrasenya incorrectes.",
"message.invalid-domain": "Domini invàlid",
"message.min-password-length": "Minimum length of {n} characters",
"message.new-version-available": "A new version of Umami {version} is available!",
"message.min-password-length": "Longitud mínima de {n} caràcters",
"message.new-version-available": "Una nova versió d'Umami {version} està disponible!",
"message.no-data-available": "No hi ha dades disponibles.",
"message.no-event-data": "No event data is available.",
"message.no-event-data": "No hi ha dades d'esdeveniments disponibles.",
"message.no-match-password": "Les contrasenyes no coincideixen",
"message.no-results-found": "No results were found.",
"message.no-team-websites": "This team does not have any websites.",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-results-found": "No s'han trobat resultats.",
"message.no-team-websites": "Aquest equip no té cap lloc web.",
"message.no-teams": "No ha creat cap equip.",
"message.no-users": "No hi ha cap usuari.",
"message.no-websites-configured": "No hi ha cap lloc web configurat.",
"message.page-not-found": "No s'ha trobat la pàgina.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website": "Per restablir aquest lloc web, escrigui {confirmation} al cuadre inferior per confirmar.",
"message.reset-website-warning": "S'esborraran totes les estadístiques per aquest lloc web, però el codi de seguiment es mantindrà.",
"message.saved": "S'ha desat amb èxit.",
"message.share-url": "Aquest és l'enllaç públic per compartir de {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
"message.team-already-member": "Ja és membre d'aquest equip.",
"message.team-not-found": "Equip no trobat.",
"message.team-websites-info": "Els llocs web poden ser visualitzats per qualsevol membre de l'equip.",
"message.tracking-code": "Codi de seguiment",
"message.transfer-team-website-to-user": "Transfer this website to your account?",
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
"message.transfer-team-website-to-user": "Transferir aquest lloc web al seu compte?",
"message.transfer-user-website-to-team": "Seleccioni l'equip al qui transferir aquest lloc web.",
"message.transfer-website": "Transferir la propietat del lloc web al seu compte o a un altre equip.",
"message.triggered-event": "Esdeveniment desencadenat",
"message.user-deleted": "Usuari eliminat.",
"message.viewed-page": "Pàgina vista",
"message.visitor-log": "Visitant de {country} usant {browser} a {os} {device}",
"message.visitors-dropped-off": "Visitors dropped off"
"message.visitors-dropped-off": "Els visitants han sortit"
}

View file

@ -1,246 +1,246 @@
{
"label.access-code": "Access code",
"label.access-code": "Erişim Kodu",
"label.actions": "Hareketler",
"label.activity-log": "Activity log",
"label.add": "Add",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.activity-log": "Aktivite Kaydı",
"label.add": "Ekle",
"label.add-description": "Açıklama ekle",
"label.add-member": "Üye ekle",
"label.add-step": "Adım ekle",
"label.add-website": "Web sitesi ekle",
"label.admin": "Yönetici",
"label.after": "After",
"label.administrator": "Yönetici",
"label.after": "Sonra",
"label.all": "Tümü",
"label.all-time": "All time",
"label.analytics": "Analytics",
"label.average": "Average",
"label.all-time": "Tüm zamanlar",
"label.analytics": "Analitik",
"label.average": "Ortalama",
"label.average-visit-time": "Ortalama ziyaret süresi",
"label.back": "Geri",
"label.before": "Before",
"label.bounce-rate": "Çıkma oranı",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.before": "Önce",
"label.bounce-rate": "Tek sayfa ziyaret oranı",
"label.breakdown": "Dağılım",
"label.browser": "Tarayıcı",
"label.browsers": "Tarayıcılar",
"label.cancel": "İptal",
"label.change-password": "Şifre değiştir",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.cities": "Şehirler",
"label.city": "Şehir",
"label.clear-all": "Hepsini temizle",
"label.confirm": "Onayla",
"label.confirm-password": "Parolayı onayla",
"label.contains": "Contains",
"label.continue": "Continue",
"label.contains": "İçeriği",
"label.continue": "Devam et",
"label.countries": "Ülkeler",
"label.country": "Country",
"label.create": "Create",
"label.create-report": "Create report",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
"label.country": "Ülke",
"label.create": "Oluştur",
"label.create-report": "Rapor oluştur",
"label.create-team": "Takım oluştur",
"label.create-user": "Kullanıcı oluştur",
"label.created": "Oluşturuldu",
"label.created-by": "Tarafından oluşturldu",
"label.current-password": "Mevcut parola",
"label.custom-range": "Özelleştirilmiş aralık",
"label.dashboard": "Kontrol Paneli",
"label.data": "Data",
"label.date": "Date",
"label.data": "Veri",
"label.date": "Tarih",
"label.date-range": "Tarih aralığı",
"label.day": "Day",
"label.day": "Gün",
"label.default-date-range": "Varsayılan tarih aralığı",
"label.delete": "Sil",
"label.delete-report": "Delete report",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-report": "Rapor sil",
"label.delete-team": "Takım sil",
"label.delete-user": "Kullanıcı sil",
"label.delete-website": "Web sitesini sil",
"label.description": "Description",
"label.description": "ıklama",
"label.desktop": "Masaüstü",
"label.details": "Details",
"label.device": "Device",
"label.details": "Detaylar",
"label.device": "Cihaz",
"label.devices": "Cihazlar",
"label.dismiss": "Reddet",
"label.does-not-contain": "Does not contain",
"label.does-not-contain": "İçermez",
"label.domain": "Alan adı",
"label.dropoff": "Dropoff",
"label.dropoff": "Bırakma",
"label.edit": "Düzenle",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
"label.edit-dashboard": "Kontrol panelini düzenle",
"label.edit-member": "Üyeyi düzenle",
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
"label.event": "Event",
"label.event-data": "Event data",
"label.event": "Olay",
"label.event-data": "Olay verisi",
"label.events": "Olaylar",
"label.false": "False",
"label.field": "Field",
"label.fields": "Fields",
"label.filter": "Filter",
"label.filter-combined": "Birleşik",
"label.filter-raw": "Ham",
"label.filters": "Filters",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
"label.false": "Yanlış",
"label.field": "Alan",
"label.fields": "Alanlar",
"label.filter": "Filtre",
"label.filter-combined": "Birleşik filtre",
"label.filter-raw": "Ham filtre",
"label.filters": "Filtreler",
"label.funnel": "Huni",
"label.funnel-description": "Kullanıcıların dönüşüm ve ayrılma oranlarını anlayın.",
"label.greater-than": "Büyüktür",
"label.greater-than-equals": "Büyük veya eşittir",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.insights-description": "Segmentleri ve filtreleri kullanarak verilerinizi derinlemesine inceleyin.",
"label.is": "Is",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Language",
"label.languages": "Languages",
"label.is-not": "Değil",
"label.is-not-set": "Ayarlanmamış",
"label.is-set": "Ayarlandı",
"label.join": "Katıl",
"label.join-team": "Takıma katıl",
"label.language": "Dil",
"label.languages": "Diller",
"label.laptop": "Dizüstü",
"label.last-days": "Son {x} gün",
"label.last-hours": "Son {x} saat",
"label.last-months": "Last {x} months",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
"label.last-months": "Son {x} ay",
"label.leave": "Ayrıl",
"label.leave-team": "Takımdan Ayrıl",
"label.less-than": "Küçüktür",
"label.less-than-equals": "Küçük veya eşittir",
"label.login": "Giriş Yap",
"label.logout": ıkış Yap",
"label.manage": "Manage",
"label.manage": "Yönet",
"label.max": "Max",
"label.member": "Member",
"label.members": "Members",
"label.member": "Üye",
"label.members": "Üyeler",
"label.min": "Min",
"label.mobile": "Mobil Cihaz",
"label.more": "Detaylı göster",
"label.my-account": "My account",
"label.my-websites": "My websites",
"label.my-account": "Hesabım",
"label.my-websites": "Web sitelerim",
"label.name": "İsim",
"label.new-password": "Yeni parola",
"label.none": "None",
"label.none": "Yok",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
"label.ok": "TAMAM",
"label.os": "OS",
"label.overview": "Overview",
"label.owner": "Owner",
"label.page-of": "Page {current} of {total}",
"label.overview": "Genel bakış",
"label.owner": "Sahibi",
"label.page-of": "{total} sayfada {current} ",
"label.page-views": "Sayfa görünümü",
"label.pageTitle": "Page title",
"label.pageTitle": "Sayfa başlığı",
"label.pages": "Sayfalar",
"label.password": "Parola",
"label.powered-by": "Sağlayıcı: {name}",
"label.profile": "Profil",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.queries": "Sorgular",
"label.query": "Sorgu",
"label.query-parameters": "Sorgu parametreleri",
"label.realtime": "Gerçek Zamanlı",
"label.referrer": "Referrer",
"label.referrers": "Yönlendirenler",
"label.refresh": "Yenile",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
"label.regenerate": "Yeniden Oluştur",
"label.region": "Bölge",
"label.regions": "Bölgeler",
"label.remove": "Kaldır",
"label.remove-member": "Üyeyi kaldır",
"label.reports": "Raporlar",
"label.required": "Zorunlu alan",
"label.reset": "Sıfırla",
"label.reset-website": "Reset statistics",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.role": "Role",
"label.run-query": "Run query",
"label.reset-website": "İstatistikleri sıfırla",
"label.retention": "Geri dönüş",
"label.retention-description": "Kullanıcıların ne sıklıkla geri döndüğünü takip ederek web sitenizin kalıcılığını ölçün.",
"label.role": "Rol",
"label.run-query": "Sorgu çalıştır",
"label.save": "Kaydet",
"label.screens": "Ekranlar",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.search": "Ara",
"label.select": "Seç",
"label.select-date": "Tarih seç",
"label.select-role": "Rol seç",
"label.select-website": "Web sitesi seç",
"label.sessions": "Sessions",
"label.settings": "Ayarlar",
"label.share-url": "Paylaşım adresi",
"label.single-day": "Tekil gün",
"label.steps": "Steps",
"label.sum": "Sum",
"label.steps": "Adımlar",
"label.sum": "Toplam",
"label.tablet": "Tablet",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
"label.theme": "Theme",
"label.team": "Takım",
"label.team-id": "Takım ID",
"label.team-member": "Takım üyesi",
"label.team-name": "Takım ismi",
"label.team-owner": "Takım sahibi",
"label.team-view-only": "Yalnızca ekip görünümü",
"label.team-websites": "Takım web siteleri",
"label.teams": "Takımlar",
"label.theme": "Tema",
"label.this-month": "Bu ay",
"label.this-week": "Bu hafta",
"label.this-year": "Bu yıl",
"label.timezone": "Zaman dilimi",
"label.title": "Title",
"label.title": "Başlık",
"label.today": "Bugün",
"label.toggle-charts": "Toggle charts",
"label.total": "Total",
"label.total-records": "Total records",
"label.toggle-charts": "Grafikleri değiştir",
"label.total": "Toplam",
"label.total-records": "Toplam kayıt",
"label.tracking-code": "İzleme kodu",
"label.transfer": "Transfer",
"label.transfer-website": "Transfer website",
"label.true": "True",
"label.type": "Type",
"label.unique": "Unique",
"label.transfer-website": "Transfer web sitesi",
"label.true": "Doğru",
"label.type": "Tip",
"label.unique": "Benzersiz",
"label.unique-visitors": "Tekil kullanıcı",
"label.unknown": "Bilinmeyen",
"label.untitled": "Untitled",
"label.update": "Update",
"label.untitled": "İsimsiz",
"label.update": "Güncelle",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "User",
"label.user": "Kullanıcı",
"label.username": "Kullanıcı adı",
"label.users": "Users",
"label.users": "Kullanıcılar",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.value": "Value",
"label.view": "View",
"label.utm-description": "Kampanyalarınızı UTM parametreleri aracılığıyla takip edin.",
"label.value": "Değer",
"label.view": "Görünüm",
"label.view-details": "Detayı incele",
"label.view-only": "View only",
"label.view-only": "Sadece görünüm",
"label.views": "Görüntüleme",
"label.views-per-visit": "Views per visit",
"label.views-per-visit": "Ziyaret başına görüntüleme",
"label.visitors": "Ziyaretçi",
"label.visits": "Visits",
"label.website": "Website",
"label.visits": "Ziyaretler",
"label.website": "Web sitesi",
"label.website-id": "Website ID",
"label.websites": "Web siteleri",
"label.window": "Window",
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"label.window": "Pencere",
"label.yesterday": "Dün",
"message.action-confirmation": "Onaylamak için aşağıdaki kutuya {confirmation} yazın.",
"message.active-users": "{x} aktif ziyaretçi",
"message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-remove": "Are you sure you want to remove {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
"message.delete-website-warning": "İlişkili tüm veriler de silinecektir.",
"message.confirm-leave": "{target} kaydından ayrılmak istediğinizden emin misiniz?",
"message.confirm-remove": "{target} kaydını kaldırmak istediğinizden emin misiniz?",
"message.confirm-reset": "{target} istatistiklerini sıfırlamak istediğinizden emin misiniz?",
"message.delete-team-warning": "Bir takımı silmek tüm takım web sitelerini de silecektir.",
"message.delete-website-warning": "İlişkili tüm veriler de silinecektir.",
"message.error": "Bir şeyler ters gitti!",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "Ayarlara git",
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
"message.invalid-domain": "Geçersiz alan adı",
"message.min-password-length": "Minimum length of {n} characters",
"message.new-version-available": "A new version of Umami {version} is available!",
"message.min-password-length": "Minimum {n} karakter uzunluğu",
"message.new-version-available": "Yeni versiyon Umami {version} mevcut!",
"message.no-data-available": "Henüz hiç veri yok.",
"message.no-event-data": "No event data is available.",
"message.no-event-data": "Hiçbir olay verisi mevcut değil.",
"message.no-match-password": "Parolalar uyuşmuyor",
"message.no-results-found": "No results were found.",
"message.no-team-websites": "This team does not have any websites.",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-results-found": "Hiçbir sonuç bulunamadı.",
"message.no-team-websites": "Bu takımın herhangi bir web sitesi yok.",
"message.no-teams": "Herhangi bir takım oluşturmadınız.",
"message.no-users": "Kullanıcı yok.",
"message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız",
"message.page-not-found": "Sayfa bulunamadı.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.reset-website": "Bu websitesini sıfılamak için aşağıdaki kutuya {confirmation} yazın.",
"message.reset-website-warning": "Bu web sitesi için tüm istatistikler silinecek, ancak izleme kodunuz bozulmadan kalacaktır.",
"message.saved": "Başarıyla kaydedildi.",
"message.share-url": "{target} için kullanılabilir anonim paylaşım adresidir.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
"message.team-already-member": "Zaten bu takımın üyesisiniz",
"message.team-not-found": "Takım bulunamadı",
"message.team-websites-info": "Web siteleri takımdaki herkes tarafından görüntülenebilir.",
"message.tracking-code": "İzleme kodu",
"message.transfer-team-website-to-user": "Transfer this website to your account?",
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
"message.transfer-team-website-to-user": "Bu web sitesi hesbınıza aktarılsın mı?",
"message.transfer-user-website-to-team": "Bu web sitesinin aktarılacağı takımı seçin.",
"message.transfer-website": "Web sitesi sahipliğini hesabınıza veya başka bir takıma aktarın",
"message.triggered-event": "Tetiklenen olay",
"message.user-deleted": "Kullanıcı silindi.",
"message.viewed-page": "Görüntülenen sayfa",
"message.visitor-log": "Yeni ziyaretçi: {country}, {os}, {device}, {browser}",
"message.visitors-dropped-off": "Visitors dropped off"
"message.visitors-dropped-off": "Bırakan ziyaretçiler"
}

View file

@ -5,7 +5,7 @@
"label.add": "添加",
"label.add-description": "添加描述",
"label.add-member": "添加成员",
"label.add-step": "Add step",
"label.add-step": "添加步骤",
"label.add-website": "添加网站",
"label.admin": "管理员",
"label.after": "之后",
@ -90,7 +90,7 @@
"label.laptop": "笔记本",
"label.last-days": "最近 {x} 天",
"label.last-hours": "最近 {x} 小时",
"label.last-months": "Last {x} months",
"label.last-months": "最近 {x} 个月",
"label.leave": "离开",
"label.leave-team": "离开团队",
"label.less-than": "少于",
@ -152,7 +152,7 @@
"label.settings": "设置",
"label.share-url": "共享链接",
"label.single-day": "单日",
"label.steps": "Steps",
"label.steps": "步骤",
"label.sum": "总和",
"label.tablet": "平板",
"label.team": "团队",
@ -182,22 +182,22 @@
"label.unique-visitors": "独立访客",
"label.unknown": "未知",
"label.untitled": "未命名",
"label.update": "Update",
"label.update": "更新",
"label.url": "网址",
"label.urls": "网址",
"label.user": "用户",
"label.username": "用户名",
"label.users": "用户",
"label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.",
"label.utm-description": "通过UTM参数追踪您的广告活动。",
"label.value": "值",
"label.view": "查看",
"label.view-details": "查看更多",
"label.view-only": "仅浏览量",
"label.views": "浏览量",
"label.views-per-visit": "Views per visit",
"label.views-per-visit": "每次访问的浏览量",
"label.visitors": "访客",
"label.visits": "Visits",
"label.visits": "访问次数",
"label.website": "网站",
"label.website-id": "网站 ID",
"label.websites": "网站",

View file

@ -1,82 +0,0 @@
import { User, Website } from '@prisma/client';
import redis from '@umami/redis-client';
import { getSession, getUser, getWebsite } from '../queries';
async function fetchWebsite(websiteId: string): Promise<Website> {
return redis.client.getCache(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
}
async function storeWebsite(data: { id: any }) {
const { id } = data;
const key = `website:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteWebsite(id) {
return redis.client.deleteCache(`website:${id}`);
}
async function fetchUser(id): Promise<User> {
return redis.client.getCache(`user:${id}`, () => getUser(id, { includePassword: true }), 86400);
}
async function storeUser(data) {
const { id } = data;
const key = `user:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteUser(id) {
return redis.client.deleteCache(`user:${id}`);
}
async function fetchSession(id) {
return redis.client.getCache(`session:${id}`, () => getSession(id), 86400);
}
async function storeSession(data) {
const { id } = data;
const key = `session:${id}`;
const obj = await redis.client.setCache(key, data);
await redis.client.expire(key, 86400);
return obj;
}
async function deleteSession(id) {
return redis.client.deleteCache(`session:${id}`);
}
async function fetchUserBlock(userId: string) {
const key = `user:block:${userId}`;
return redis.client.get(key);
}
async function incrementUserBlock(userId: string) {
const key = `user:block:${userId}`;
return redis.client.incr(key);
}
export default {
fetchWebsite,
storeWebsite,
deleteWebsite,
fetchUser,
storeUser,
deleteUser,
fetchSession,
storeSession,
deleteSession,
fetchUserBlock,
incrementUserBlock,
enabled: !!redis.enabled,
};

View file

@ -4,7 +4,7 @@ import debug from 'debug';
import { CLICKHOUSE } from 'lib/db';
import { QueryFilters, QueryOptions } from './types';
import { OPERATORS } from './constants';
import { loadWebsite } from './load';
import { fetchWebsite } from './load';
import { maxDate } from './date';
import { filtersToArray } from './params';
@ -106,7 +106,7 @@ function getFilterParams(filters: QueryFilters = {}) {
}
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
return {
filterQuery: getFilterQuery(filters, options),

View file

@ -30,7 +30,8 @@ export const FILTER_DAY = 'filter-day';
export const FILTER_RANGE = 'filter-range';
export const FILTER_REFERRERS = 'filter-referrers';
export const FILTER_PAGES = 'filter-pages';
export const UNIT_TYPES = ['year', 'month', 'hour', 'day'];
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event', 'host'];
export const SESSION_COLUMNS = [
@ -134,6 +135,7 @@ export const ROLES = {
user: 'user',
viewOnly: 'view-only',
teamOwner: 'team-owner',
teamManager: 'team-manager',
teamMember: 'team-member',
teamViewOnly: 'team-view-only',
} as const;
@ -164,6 +166,12 @@ export const ROLE_PERMISSIONS = {
PERMISSIONS.websiteUpdate,
PERMISSIONS.websiteDelete,
],
[ROLES.teamManager]: [
PERMISSIONS.teamUpdate,
PERMISSIONS.websiteCreate,
PERMISSIONS.websiteUpdate,
PERMISSIONS.websiteDelete,
],
[ROLES.teamMember]: [
PERMISSIONS.websiteCreate,
PERMISSIONS.websiteUpdate,

View file

@ -1,6 +1,6 @@
import { startOfHour, startOfMonth } from 'date-fns';
import { hash } from 'next-basics';
import { v4, v5, validate } from 'uuid';
import { v4, v5 } from 'uuid';
export function secret() {
return hash(process.env.APP_SECRET || process.env.DATABASE_URL);
@ -23,7 +23,3 @@ export function uuid(...args: any) {
return v5(hash(...args, salt()), v5.DNS);
}
export function isUuid(value: string) {
return validate(value);
}

View file

@ -132,7 +132,7 @@ export async function getClientInfo(req: NextApiRequestCollect) {
const subdivision2 = location?.subdivision2;
const city = location?.city;
const browser = browserName(userAgent);
const os = detectOS(userAgent);
const os = detectOS(userAgent) as string;
const device = getDevice(req.body?.payload?.screen, os);
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };

View file

@ -2,6 +2,7 @@ import {
arSA,
be,
bn,
bs,
cs,
sk,
da,
@ -48,6 +49,7 @@ export const languages = {
'ar-SA': { label: 'العربية', dateLocale: arSA, dir: 'rtl' },
'be-BY': { label: 'Беларуская', dateLocale: be },
'bn-BD': { label: 'বাংলা', dateLocale: bn },
'bs-BA': { label: 'Bosanski', dateLocale: bs },
'ca-ES': { label: 'Català', dateLocale: ca },
'cs-CZ': { label: 'Čeština', dateLocale: cs },
'da-DK': { label: 'Dansk', dateLocale: da },

View file

@ -1,12 +1,12 @@
import cache from 'lib/cache';
import { getSession, getUser, getWebsite } from 'queries';
import { User, Website, Session } from '@prisma/client';
import { getSession, getWebsite } from 'queries';
import { Website, Session } from '@prisma/client';
import redis from '@umami/redis-client';
export async function loadWebsite(websiteId: string): Promise<Website> {
export async function fetchWebsite(websiteId: string): Promise<Website> {
let website;
if (cache.enabled) {
website = await cache.fetchWebsite(websiteId);
if (redis.enabled) {
website = await redis.client.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
} else {
website = await getWebsite(websiteId);
}
@ -18,11 +18,11 @@ export async function loadWebsite(websiteId: string): Promise<Website> {
return website;
}
export async function loadSession(sessionId: string): Promise<Session> {
export async function fetchSession(sessionId: string): Promise<Session> {
let session;
if (cache.enabled) {
session = await cache.fetchSession(sessionId);
if (redis.enabled) {
session = await redis.client.fetch(`session:${sessionId}`, () => getSession(sessionId), 86400);
} else {
session = await getSession(sessionId);
}
@ -33,19 +33,3 @@ export async function loadSession(sessionId: string): Promise<Session> {
return session;
}
export async function loadUser(userId: string): Promise<User> {
let user;
if (cache.enabled) {
user = await cache.fetchUser(userId);
} else {
user = await getUser(userId);
}
if (!user || user.deletedAt) {
return null;
}
return user;
}

View file

@ -4,13 +4,12 @@ import redis from '@umami/redis-client';
import { getAuthToken, parseShareToken } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { secret } from 'lib/crypto';
import { findSession } from 'lib/session';
import { getSession } from 'lib/session';
import {
badRequest,
createMiddleware,
forbidden,
notFound,
parseSecureToken,
tooManyRequest,
unauthorized,
} from 'next-basics';
import { NextApiRequestCollect } from 'pages/api/send';
@ -27,7 +26,7 @@ export const useCors = createMiddleware(
export const useSession = createMiddleware(async (req, res, next) => {
try {
const session = await findSession(req as NextApiRequestCollect);
const session = await getSession(req as NextApiRequestCollect);
if (!session) {
log('useSession: Session not found');
@ -36,11 +35,8 @@ export const useSession = createMiddleware(async (req, res, next) => {
(req as any).session = session;
} catch (e: any) {
if (e.message === 'Usage Limit.') {
return tooManyRequest(res, e.message);
}
if (e.message.startsWith('Website not found:')) {
return forbidden(res, e.message);
if (e.message.startsWith('Website not found')) {
return notFound(res, e.message);
}
return badRequest(res, e.message);
}

View file

@ -3,9 +3,9 @@ import prisma from '@umami/prisma-client';
import moment from 'moment-timezone';
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
import { loadWebsite } from './load';
import { fetchWebsite } from './load';
import { maxDate } from './date';
import { QueryFilters, QueryOptions, SearchFilter } from './types';
import { QueryFilters, QueryOptions, PageParams } from './types';
import { filtersToArray } from './params';
const MYSQL_DATE_FORMATS = {
@ -152,7 +152,7 @@ async function parseFilters(
filters: QueryFilters = {},
options: QueryOptions = {},
) {
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
const joinSession = Object.keys(filters).find(key => SESSION_COLUMNS.includes(key));
return {
@ -191,7 +191,7 @@ async function rawQuery(sql: string, data: object): Promise<any> {
return prisma.rawQuery(query, params);
}
async function pagedQuery<T>(model: string, criteria: T, filters: SearchFilter) {
async function pagedQuery<T>(model: string, criteria: T, filters: PageParams) {
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {};
const size = +pageSize || DEFAULT_PAGE_SIZE;

View file

@ -2,7 +2,7 @@ import * as yup from 'yup';
export const dateRange = {
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
};
export const pageInfo = {

View file

@ -1,28 +1,13 @@
import { isUuid, secret, uuid, visitSalt } from 'lib/crypto';
import { secret, uuid, visitSalt } from 'lib/crypto';
import { getClientInfo } from 'lib/detect';
import { parseToken } from 'next-basics';
import { NextApiRequestCollect } from 'pages/api/send';
import { createSession } from 'queries';
import cache from './cache';
import clickhouse from './clickhouse';
import { loadSession, loadWebsite } from './load';
import { fetchSession, fetchWebsite } from './load';
import { SessionData } from 'lib/types';
export async function findSession(req: NextApiRequestCollect): Promise<{
id: any;
websiteId: string;
visitId: string;
hostname: string;
browser: string;
os: any;
device: string;
screen: string;
language: string;
country: any;
subdivision1: any;
subdivision2: any;
city: any;
ownerId: string;
}> {
export async function getSession(req: NextApiRequestCollect): Promise<SessionData> {
const { payload } = req.body;
if (!payload) {
@ -35,9 +20,8 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
if (cacheToken) {
const result = await parseToken(cacheToken, secret());
// Token is valid
if (result) {
await checkUserBlock(result?.ownerId);
return result;
}
}
@ -45,25 +29,13 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
// Verify payload
const { website: websiteId, hostname, screen, language } = payload;
// Check the hostname value for legality to eliminate dirty data
const validHostnameRegex = /^[\w-.]+$/;
if (!validHostnameRegex.test(hostname)) {
throw new Error('Invalid hostname.');
}
if (!isUuid(websiteId)) {
throw new Error('Invalid website ID.');
}
// Find website
const website = await loadWebsite(websiteId);
const website = await fetchWebsite(websiteId);
if (!website) {
throw new Error(`Website not found: ${websiteId}.`);
}
await checkUserBlock(website.userId);
const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } =
await getClientInfo(req);
@ -78,7 +50,7 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
visitId,
hostname,
browser,
os: os as any,
os,
device,
screen,
language,
@ -86,12 +58,11 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
subdivision1,
subdivision2,
city,
ownerId: website.userId,
};
}
// Find session
let session = await loadSession(sessionId);
let session = await fetchSession(sessionId);
// Create a session if not found
if (!session) {
@ -117,13 +88,5 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
}
}
return { ...session, ownerId: website.userId, visitId: visitId };
}
async function checkUserBlock(userId: string) {
if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) {
await cache.incrementUserBlock(userId);
throw new Error('Usage Limit.');
}
return { ...session, visitId: visitId };
}

View file

@ -24,31 +24,7 @@ export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
export type ReportType = ObjectValues<typeof REPORT_TYPES>;
export interface WebsiteSearchFilter extends SearchFilter {
userId?: string;
teamId?: string;
includeTeams?: boolean;
onlyTeams?: boolean;
}
export interface UserSearchFilter extends SearchFilter {
teamId?: string;
}
export interface TeamSearchFilter extends SearchFilter {
userId?: string;
}
export interface TeamUserSearchFilter extends SearchFilter {
teamId?: string;
}
export interface ReportSearchFilter extends SearchFilter {
userId?: string;
websiteId?: string;
}
export interface SearchFilter {
export interface PageParams {
query?: string;
page?: number;
pageSize?: number;
@ -56,7 +32,7 @@ export interface SearchFilter {
sortDescending?: boolean;
}
export interface FilterResult<T> {
export interface PageResult<T> {
data: T;
count: number;
page: number;
@ -66,10 +42,10 @@ export interface FilterResult<T> {
}
export interface FilterQueryResult<T> {
result: FilterResult<T>;
result: PageResult<T>;
query: any;
params: SearchFilter;
setParams: Dispatch<SetStateAction<T | SearchFilter>>;
params: PageParams;
setParams: Dispatch<SetStateAction<T | PageParams>>;
}
export interface DynamicData {
@ -230,3 +206,19 @@ export interface RealtimeData {
countries?: any[];
visitors?: any[];
}
export interface SessionData {
id: string;
websiteId: string;
visitId: string;
hostname: string;
browser: string;
os: string;
device: string;
screen: string;
language: string;
country: string;
subdivision1: string;
subdivision2: string;
city: string;
}

View file

@ -1,13 +1,13 @@
import { canViewUsers } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types';
import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getUsers } from 'queries';
import * as yup from 'yup';
export interface UsersRequestQuery extends SearchFilter {}
export interface UsersRequestQuery extends PageParams {}
export interface UsersRequestBody {
userId: string;
username: string;

View file

@ -1,13 +1,17 @@
import { canViewAllWebsites } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsites } from 'queries';
import * as yup from 'yup';
import { pageInfo } from 'lib/schema';
export interface WebsitesRequestQuery extends SearchFilter {}
export interface WebsitesRequestQuery extends PageParams {
userId?: string;
includeTeams?: boolean;
}
export interface WebsitesRequestBody {
name: string;
@ -39,8 +43,29 @@ export default async (
return unauthorized(res);
}
const { userId, includeOwnedTeams } = req.query;
const websites = await getWebsites(
{
where: {
OR: [
...(userId && [{ userId }]),
...(userId &&
includeOwnedTeams && [
{
team: {
deletedAt: null,
teamUser: {
some: {
role: ROLES.teamOwner,
userId,
},
},
},
},
]),
],
},
include: {
user: {
select: {
@ -48,6 +73,18 @@ export default async (
id: true,
},
},
team: {
where: {
deletedAt: null,
},
include: {
teamUser: {
where: {
role: ROLES.teamOwner,
},
},
},
},
},
},
req.query,

View file

@ -17,7 +17,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
event: yup.string(),
}),
};

View file

@ -17,7 +17,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
field: yup.string(),
}),
};

View file

@ -16,7 +16,7 @@ const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
}),
};

View file

@ -1,10 +1,5 @@
import ipaddr from 'ipaddr.js';
import { isbot } from 'isbot';
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
import { secret, visitSalt, uuid } from 'lib/crypto';
import { getIpAddress } from 'lib/detect';
import { useCors, useSession, useValidate } from 'lib/middleware';
import { CollectionType, YupRequest } from 'lib/types';
import { NextApiRequest, NextApiResponse } from 'next';
import {
badRequest,
@ -15,6 +10,11 @@ import {
safeDecodeURI,
send,
} from 'next-basics';
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
import { secret, visitSalt, uuid } from 'lib/crypto';
import { getIpAddress } from 'lib/detect';
import { useCors, useSession, useValidate } from 'lib/middleware';
import { CollectionType, YupRequest } from 'lib/types';
import { saveEvent, saveSessionData } from 'queries';
import * as yup from 'yup';
@ -41,7 +41,6 @@ export interface NextApiRequestCollect extends NextApiRequest {
id: string;
websiteId: string;
visitId: string;
ownerId: string;
hostname: string;
browser: string;
os: string;

View file

@ -23,7 +23,7 @@ const schema = {
POST: yup.object().shape({
role: yup
.string()
.matches(/team-member|team-view-only/i)
.matches(/team-member|team-view-only|team-manager/i)
.required(),
}),
};

View file

@ -1,13 +1,13 @@
import { canAddUserToTeam, canViewTeam } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { pageInfo } from 'lib/schema';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeamUser, getTeamUser, getTeamUsers } from 'queries';
import * as yup from 'yup';
export interface TeamUserRequestQuery extends SearchFilter {
export interface TeamUserRequestQuery extends PageParams {
teamId: string;
}
@ -25,7 +25,7 @@ const schema = {
userId: yup.string().uuid().required(),
role: yup
.string()
.matches(/team-member|team-view-only/i)
.matches(/team-member|team-view-only|team-manager/i)
.required(),
}),
};

View file

@ -1,13 +1,13 @@
import * as yup from 'yup';
import { canViewTeam } from 'lib/auth';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { ok, unauthorized } from 'next-basics';
import { getTeamWebsites } from 'queries';
export interface TeamWebsiteRequestQuery extends SearchFilter {
export interface TeamWebsiteRequestQuery extends PageParams {
teamId: string;
}

View file

@ -3,13 +3,13 @@ import { Team } from '@prisma/client';
import { canCreateTeam } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeam } from 'queries';
export interface TeamsRequestQuery extends SearchFilter {}
export interface TeamsRequestQuery extends PageParams {}
export interface TeamsRequestBody {
name: string;
}

View file

@ -1,12 +1,12 @@
import * as yup from 'yup';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getUserTeams } from 'queries';
export interface UserTeamsRequestQuery extends SearchFilter {
export interface UserTeamsRequestQuery extends PageParams {
userId: string;
}

View file

@ -26,7 +26,7 @@ const schema = {
GET: yup.object().shape({
id: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref<number>('startAt')).required(),
endAt: yup.number().integer().min(yup.ref<number>('startAt')).required(),
}),
};

View file

@ -2,14 +2,14 @@ import { canCreateUser } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types';
import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createUser, getUserByUsername } from 'queries';
import * as yup from 'yup';
export interface UsersRequestQuery extends SearchFilter {}
export interface UsersRequestQuery extends PageParams {}
export interface UsersRequestBody {
username: string;
password: string;

View file

@ -1,6 +1,6 @@
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { getRequestDateRange } from 'lib/request';
import { getRequestFilters, getRequestDateRange } from 'lib/request';
import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types';
import { TimezoneTest, UnitTypeTest } from 'lib/yup';
import { NextApiResponse } from 'next';
@ -15,16 +15,32 @@ export interface WebsiteEventsRequestQuery {
unit?: string;
timezone?: string;
url: string;
referrer?: string;
title?: string;
os?: string;
browser?: string;
device?: string;
country?: string;
region: string;
city?: string;
}
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
unit: UnitTypeTest,
timezone: TimezoneTest,
url: yup.string(),
referrer: yup.string(),
title: yup.string(),
os: yup.string(),
browser: yup.string(),
device: yup.string(),
country: yup.string(),
region: yup.string(),
city: yup.string(),
}),
};
@ -36,7 +52,7 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId, timezone, url } = req.query;
const { websiteId, timezone } = req.query;
const { startDate, endDate, unit } = await getRequestDateRange(req);
if (req.method === 'GET') {
@ -44,13 +60,15 @@ export default async (
return unauthorized(res);
}
const events = await getEventMetrics(websiteId, {
const filters = {
...getRequestFilters(req),
startDate,
endDate,
timezone,
unit,
url,
});
};
const events = await getEventMetrics(websiteId, filters);
return ok(res, events);
}

View file

@ -1,13 +1,13 @@
import * as yup from 'yup';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsiteReports } from 'queries';
import { pageInfo } from 'lib/schema';
export interface ReportsRequestQuery extends SearchFilter {
export interface ReportsRequestQuery extends PageParams {
websiteId: string;
}

View file

@ -1,7 +1,7 @@
import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createWebsite } from 'queries';
@ -9,7 +9,7 @@ import userWebsitesRoute from 'pages/api/users/[userId]/websites';
import * as yup from 'yup';
import { pageInfo } from 'lib/schema';
export interface WebsitesRequestQuery extends SearchFilter {}
export interface WebsitesRequestQuery extends PageParams {}
export interface WebsitesRequestBody {
name: string;

View file

@ -1,6 +1,6 @@
import { Prisma, Report } from '@prisma/client';
import prisma from 'lib/prisma';
import { FilterResult, ReportSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise<Report> {
@ -17,8 +17,8 @@ export async function getReport(reportId: string): Promise<Report> {
export async function getReports(
criteria: ReportFindManyArgs,
filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> {
filters: PageParams = {},
): Promise<PageResult<Report[]>> {
const { query } = filters;
const where: Prisma.ReportWhereInput = {
@ -50,8 +50,8 @@ export async function getReports(
export async function getUserReports(
userId: string,
filters?: ReportSearchFilter,
): Promise<FilterResult<Report[]>> {
filters?: PageParams,
): Promise<PageResult<Report[]>> {
return getReports(
{
where: {
@ -72,8 +72,8 @@ export async function getUserReports(
export async function getWebsiteReports(
websiteId: string,
filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> {
filters: PageParams = {},
): Promise<PageResult<Report[]>> {
return getReports(
{
where: {

View file

@ -2,7 +2,7 @@ import { Prisma, Team } from '@prisma/client';
import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
import { FilterResult, TeamSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import TeamFindManyArgs = Prisma.TeamFindManyArgs;
export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise<Team> {
@ -22,8 +22,8 @@ export async function getTeam(teamId: string, options: { includeMembers?: boolea
export async function getTeams(
criteria: TeamFindManyArgs,
filters: TeamSearchFilter = {},
): Promise<FilterResult<Team[]>> {
filters: PageParams = {},
): Promise<PageResult<Team[]>> {
const { getSearchParameters } = prisma;
const { query } = filters;
@ -42,7 +42,7 @@ export async function getTeams(
);
}
export async function getUserTeams(userId: string, filters: TeamSearchFilter = {}) {
export async function getUserTeams(userId: string, filters: PageParams = {}) {
return getTeams(
{
where: {

View file

@ -1,7 +1,7 @@
import { Prisma, TeamUser } from '@prisma/client';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
import { FilterResult, TeamUserSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs;
export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise<TeamUser> {
@ -19,8 +19,8 @@ export async function getTeamUser(teamId: string, userId: string): Promise<TeamU
export async function getTeamUsers(
criteria: TeamUserFindManyArgs,
filters?: TeamUserSearchFilter,
): Promise<FilterResult<TeamUser[]>> {
filters?: PageParams,
): Promise<PageResult<TeamUser[]>> {
const { query } = filters;
const where: Prisma.TeamUserWhereInput = {

View file

@ -1,8 +1,7 @@
import { Prisma } from '@prisma/client';
import cache from 'lib/cache';
import { ROLES } from 'lib/constants';
import prisma from 'lib/prisma';
import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
import { PageResult, Role, User, PageParams } from 'lib/types';
import { getRandomChars } from 'next-basics';
import UserFindManyArgs = Prisma.UserFindManyArgs;
@ -50,8 +49,8 @@ export async function getUserByUsername(username: string, options: GetUserOption
export async function getUsers(
criteria: UserFindManyArgs,
filters?: UserSearchFilter,
): Promise<FilterResult<User[]>> {
filters?: PageParams,
): Promise<PageResult<User[]>> {
const { query } = filters;
const where: Prisma.UserWhereInput = {
@ -221,15 +220,5 @@ export async function deleteUser(
id: userId,
},
}),
]).then(async data => {
if (cache.enabled) {
const ids = websites.map(a => a.id);
for (let i = 0; i < ids.length; i++) {
await cache.deleteWebsite(`website:${ids[i]}`);
}
}
return data;
});
]);
}

View file

@ -1,7 +1,6 @@
import { Prisma, Website } from '@prisma/client';
import cache from 'lib/cache';
import prisma from 'lib/prisma';
import { FilterResult, WebsiteSearchFilter } from 'lib/types';
import { PageResult, PageParams } from 'lib/types';
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise<Website> {
@ -26,8 +25,8 @@ export async function getSharedWebsite(shareId: string) {
export async function getWebsites(
criteria: WebsiteFindManyArgs,
filters: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters: PageParams,
): Promise<PageResult<Website[]>> {
const { query } = filters;
const where: Prisma.WebsiteWhereInput = {
@ -54,8 +53,8 @@ export async function getAllWebsites(userId: string) {
export async function getUserWebsites(
userId: string,
filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters?: PageParams,
): Promise<PageResult<Website[]>> {
return getWebsites(
{
where: {
@ -79,8 +78,8 @@ export async function getUserWebsites(
export async function getTeamWebsites(
teamId: string,
filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
filters?: PageParams,
): Promise<PageResult<Website[]>> {
return getWebsites(
{
where: {
@ -102,17 +101,9 @@ export async function getTeamWebsites(
export async function createWebsite(
data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput,
): Promise<Website> {
return prisma.client.website
.create({
data,
})
.then(async data => {
if (cache.enabled) {
await cache.storeWebsite(data);
}
return data;
});
return prisma.client.website.create({
data,
});
}
export async function updateWebsite(
@ -148,13 +139,7 @@ export async function resetWebsite(
resetAt: new Date(),
},
}),
]).then(async data => {
if (cache.enabled) {
await cache.storeWebsite(data[3]);
}
return data;
});
]);
}
export async function deleteWebsite(
@ -188,11 +173,5 @@ export async function deleteWebsite(
: client.website.delete({
where: { id: websiteId },
}),
]).then(async data => {
if (cache.enabled) {
await cache.deleteWebsite(websiteId);
}
return data;
});
]);
}

View file

@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
and event_data.created_at between {{startDate}} and {{endDate}}
and website_event.event_name = {{event}}
group by website_event.event_name, event_data.data_key, event_data.data_type, event_data.string_value
order by 1 asc, 2 asc, 3 asc, 4 desc
order by 1 asc, 2 asc, 3 asc, 5 desc
`,
params,
);
@ -81,7 +81,7 @@ async function clickhouseQuery(
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_name = {event:String}
group by data_key, data_type, string_value, event_name
order by 1 asc, 2 asc, 3 asc, 4 desc
order by 1 asc, 2 asc, 3 asc, 5 desc
limit 500
`,
params,

View file

@ -25,12 +25,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
`
select
event_name x,
${getDateQuery('created_at', unit, timezone)} t,
${getDateQuery('website_event.created_at', unit, timezone)} t,
count(*) y
from website_event
${joinSession}
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1, 2

View file

@ -1,5 +1,4 @@
import { Prisma } from '@prisma/client';
import cache from 'lib/cache';
import prisma from 'lib/prisma';
export async function createSession(data: Prisma.SessionCreateInput) {
@ -18,28 +17,20 @@ export async function createSession(data: Prisma.SessionCreateInput) {
city,
} = data;
return prisma.client.session
.create({
data: {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
},
})
.then(async data => {
if (cache.enabled) {
await cache.storeSession(data);
}
return data;
});
return prisma.client.session.create({
data: {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
},
});
}