Merge branch 'master' of github.com:umami-software/umami

This commit is contained in:
Matthias Kretschmann 2023-08-29 18:43:50 +01:00
commit b2c5b83f84
Signed by: m
GPG Key ID: 606EEEF3C479A91F
693 changed files with 47586 additions and 11121 deletions

View File

@ -19,22 +19,21 @@
"plugin:@typescript-eslint/recommended",
"next"
],
"plugins": ["@typescript-eslint", "prettier"],
"settings": {
"import/resolver": {
"alias": {
"map": [
["assets", "./assets"],
["components", "./components"],
["assets", "./src/assets"],
["components", "./src/components"],
["db", "./db"],
["hooks", "./hooks"],
["lang", "./lang"],
["lib", "./lib"],
["hooks", "./src/components/hooks"],
["lang", "./src/lang"],
["lib", "./src/lib"],
["public", "./public"],
["queries", "./queries"],
["store", "./store"],
["styles", "./styles"]
["queries", "./src/queries"],
["store", "./src/store"],
["styles", "./src/styles"]
],
"extensions": [".ts", ".tsx", ".js", ".jsx", ".json"]
}
@ -50,7 +49,9 @@
"@next/next/no-img-element": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off"
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
},
"globals": {
"React": "writable"

View File

@ -1,6 +1,5 @@
name: "✨ Feature Request"
name: '✨ Feature Request'
description: Create a feature or enhancement request for Umami.
labels: ['enhancement']
body:
- type: textarea
attributes:

19
.github/stale.yml vendored
View File

@ -1,19 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- enhancement
- bug
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v3
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image for ${{ matrix.db-type }}
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
with:
image: umami
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest
@ -31,3 +31,13 @@ jobs:
platform: linux/amd64,linux/arm64
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with:
image: umamisoftware/umami
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@ -19,7 +19,7 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image for ${{ matrix.db-type }}
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
with:
image: umami
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
@ -29,3 +29,13 @@ jobs:
platform: linux/amd64,linux/arm64
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with:
image: umamisoftware/umami
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@ -16,10 +16,6 @@ jobs:
strategy:
matrix:
include:
- node-version: 14.x
db-type: postgresql
- node-version: 14.x
db-type: mysql
- node-version: 16.x
db-type: postgresql
- node-version: 16.x
@ -28,6 +24,7 @@ jobs:
db-type: postgresql
- node-version: 18.x
db-type: mysql
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}

24
.github/workflows/stale-issues.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Close stale issues
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v8
with:
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'stale'
stale-issue-message: 'This issue is stale because it has been open for 60 days with no activity.'
close-issue-message: 'This issue was closed because it has been inactive for 7 days since being marked as stale.'
days-before-pr-stale: -1
days-before-pr-close: -1
operations-per-run: 200
ascending: true
repo-token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ node_modules
*.iml
*.log
.vscode
.tool-versions
# debug
npm-debug.log*

View File

@ -1 +0,0 @@
<svg id="Layer_2" height="512" viewBox="0 0 30 30" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 2"><g fill="rgb(0,0,0)"><path d="m15 14a5.5 5.5 0 1 1 5.5-5.5 5.51 5.51 0 0 1 -5.5 5.5zm0-9a3.5 3.5 0 1 0 3.5 3.5 3.5 3.5 0 0 0 -3.5-3.5z"/><path d="m7.5 24.5a1 1 0 0 1 -1-1 8.5 8.5 0 0 1 13.6-6.8 1 1 0 1 1 -1.2 1.6 6.44 6.44 0 0 0 -3.9-1.3 6.51 6.51 0 0 0 -6.5 6.5 1 1 0 0 1 -1 1z"/><path d="m23 27a1 1 0 0 1 -1-1v-6a1 1 0 0 1 2 0v6a1 1 0 0 1 -1 1z"/><path d="m26 24h-6a1 1 0 0 1 0-2h6a1 1 0 0 1 0 2z"/></g></svg>

Before

Width:  |  Height:  |  Size: 529 B

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="M197.332 170.668h-160C16.746 170.668 0 153.922 0 133.332v-96C0 16.746 16.746 0 37.332 0h160c20.59 0 37.336 16.746 37.336 37.332v96c0 20.59-16.746 37.336-37.336 37.336zM37.332 32A5.336 5.336 0 0 0 32 37.332v96a5.337 5.337 0 0 0 5.332 5.336h160a5.338 5.338 0 0 0 5.336-5.336v-96A5.337 5.337 0 0 0 197.332 32zM197.332 512h-160C16.746 512 0 495.254 0 474.668v-224c0-20.59 16.746-37.336 37.332-37.336h160c20.59 0 37.336 16.746 37.336 37.336v224c0 20.586-16.746 37.332-37.336 37.332zm-160-266.668A5.337 5.337 0 0 0 32 250.668v224A5.336 5.336 0 0 0 37.332 480h160a5.337 5.337 0 0 0 5.336-5.332v-224a5.338 5.338 0 0 0-5.336-5.336zM474.668 512h-160c-20.59 0-37.336-16.746-37.336-37.332v-96c0-20.59 16.746-37.336 37.336-37.336h160c20.586 0 37.332 16.746 37.332 37.336v96C512 495.254 495.254 512 474.668 512zm-160-138.668a5.338 5.338 0 0 0-5.336 5.336v96a5.337 5.337 0 0 0 5.336 5.332h160a5.336 5.336 0 0 0 5.332-5.332v-96a5.337 5.337 0 0 0-5.332-5.336zM474.668 298.668h-160c-20.59 0-37.336-16.746-37.336-37.336v-224C277.332 16.746 294.078 0 314.668 0h160C495.254 0 512 16.746 512 37.332v224c0 20.59-16.746 37.336-37.332 37.336zM314.668 32a5.337 5.337 0 0 0-5.336 5.332v224a5.338 5.338 0 0 0 5.336 5.336h160a5.337 5.337 0 0 0 5.332-5.336v-224A5.336 5.336 0 0 0 474.668 32zm0 0"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,43 +0,0 @@
.chart {
position: relative;
}
.tooltip {
position: fixed;
pointer-events: none;
z-index: var(--z-index-popup);
}
.content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.title {
font-size: var(--font-size-xs);
font-weight: 600;
}
.metric {
display: flex;
justify-content: center;
align-items: center;
font-size: var(--font-size-sm);
font-weight: 600;
}
.dot {
position: relative;
overflow: hidden;
border-radius: 100%;
margin-right: 8px;
background: var(--base50);
}
.color {
width: 10px;
height: 10px;
}

View File

@ -1,15 +0,0 @@
import classNames from 'classnames';
import styles from './NoData.module.css';
import useMessages from 'hooks/useMessages';
export function NoData({ className }) {
const { formatMessage, messages } = useMessages();
return (
<div className={classNames(styles.container, className)}>
{formatMessage(messages.noDataAvailable)}
</div>
);
}
export default NoData;

View File

@ -1,38 +0,0 @@
import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics';
import styles from './SettingsTable.module.css';
export function SettingsTable({ columns = [], data = [], children, cellRender }) {
return (
<Table columns={columns} rows={data}>
<TableHeader className={styles.header}>
{(column, index) => {
return (
<TableColumn key={index} className={styles.cell} style={columns[index].style}>
{column.label}
</TableColumn>
);
}}
</TableHeader>
<TableBody className={styles.body}>
{(row, keys, rowIndex) => {
row.action = children(row, keys, rowIndex);
return (
<TableRow key={rowIndex} data={row} keys={keys} className={styles.row}>
{(data, key, colIndex) => {
return (
<TableCell key={colIndex} className={styles.cell} style={columns[colIndex].style}>
<label className={styles.label}>{columns[colIndex].label}</label>
{cellRender ? cellRender(row, data, key, colIndex) : data[key]}
</TableCell>
);
}}
</TableRow>
);
}}
</TableBody>
</Table>
);
}
export default SettingsTable;

View File

@ -1,47 +0,0 @@
import { Icon, Button, PopupTrigger, Popup, Text } from 'react-basics';
import classNames from 'classnames';
import { languages } from 'lib/lang';
import useLocale from 'hooks/useLocale';
import Icons from 'components/icons';
import styles from './LanguageButton.module.css';
export function LanguageButton() {
const { locale, saveLocale, dir } = useLocale();
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
function handleSelect(value) {
saveLocale(value);
}
return (
<PopupTrigger>
<Button variant="quiet">
<Icon>
<Icons.Globe />
</Icon>
</Button>
<Popup position="bottom" alignment={dir === 'rtl' ? 'start' : 'end'}>
<div className={styles.menu}>
{items.map(({ value, label }) => {
return (
<div
key={value}
className={classNames(styles.item, { [styles.selected]: value === locale })}
onClick={handleSelect.bind(null, value)}
>
<Text>{label}</Text>
{value === locale && (
<Icon className={styles.icon}>
<Icons.Check />
</Icon>
)}
</div>
);
})}
</div>
</Popup>
</PopupTrigger>
);
}
export default LanguageButton;

View File

@ -1,32 +0,0 @@
import { LoadingButton, Icon, Tooltip } from 'react-basics';
import { setWebsiteDateRange } from 'store/websites';
import useDateRange from 'hooks/useDateRange';
import Icons from 'components/icons';
import useMessages from 'hooks/useMessages';
export function RefreshButton({ websiteId, isLoading }) {
const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId);
function handleClick() {
if (!isLoading && dateRange) {
if (/^\d+/.test(dateRange.value)) {
setWebsiteDateRange(websiteId, dateRange.value);
} else {
setWebsiteDateRange(websiteId, dateRange);
}
}
}
return (
<Tooltip label={formatMessage(labels.refresh)}>
<LoadingButton loading={isLoading} onClick={handleClick}>
<Icon>
<Icons.Refresh />
</Icon>
</LoadingButton>
</Tooltip>
);
}
export default RefreshButton;

View File

@ -1,33 +0,0 @@
import { Row, Column } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import { CURRENT_VERSION, HOMEPAGE_URL, REPO_URL } from 'lib/constants';
import { labels } from 'components/messages';
import styles from './Footer.module.css';
export function Footer() {
return (
<footer className={styles.footer}>
<Row>
<Column defaultSize={12} lg={11} xl={11}>
<div>
<FormattedMessage
{...labels.poweredBy}
values={{
name: (
<a href={HOMEPAGE_URL}>
<b>umami</b>
</a>
),
}}
/>
</div>
</Column>
<Column className={styles.version} defaultSize={12} lg={1} xl={1}>
<a href={REPO_URL}>{`v${CURRENT_VERSION}`}</a>
</Column>
</Row>
</footer>
);
}
export default Footer;

View File

@ -1,16 +0,0 @@
.footer {
font-size: var(--font-size-sm);
text-align: center;
line-height: 30px;
margin: 60px 0;
}
.footer a {
color: var(--font-color100);
}
.version {
text-align: right;
padding-right: 10px;
white-space: nowrap;
}

View File

@ -1,13 +0,0 @@
import React from 'react';
import styles from './PageHeader.module.css';
export function PageHeader({ title, children }) {
return (
<div className={styles.header}>
<div className={styles.title}>{title}</div>
<div className={styles.actions}>{children}</div>
</div>
);
}
export default PageHeader;

View File

@ -1,214 +0,0 @@
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { StatusLight, Loading } from 'react-basics';
import classNames from 'classnames';
import Chart from 'chart.js/auto';
import HoverTooltip from 'components/common/HoverTooltip';
import Legend from 'components/metrics/Legend';
import { formatLongNumber } from 'lib/format';
import { dateFormat } from 'lib/date';
import useLocale from 'hooks/useLocale';
import useTheme from 'hooks/useTheme';
import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
import styles from './BarChart.module.css';
export function BarChart({
datasets,
unit,
animationDuration = DEFAULT_ANIMATION_DURATION,
stacked = false,
loading = false,
onCreate = () => {},
onUpdate = () => {},
className,
}) {
const canvas = useRef();
const chart = useRef(null);
const [tooltip, setTooltip] = useState(null);
const { locale } = useLocale();
const [theme] = useTheme();
const colors = useMemo(
() => ({
text: THEME_COLORS[theme].gray700,
line: THEME_COLORS[theme].gray200,
}),
[theme],
);
const renderYLabel = label => {
return +label > 1000 ? formatLongNumber(label) : label;
};
const renderXLabel = useCallback(
(label, index, values) => {
const d = new Date(values[index].value);
switch (unit) {
case 'minute':
return dateFormat(d, 'h:mm', locale);
case 'hour':
return dateFormat(d, 'p', locale);
case 'day':
return dateFormat(d, 'MMM d', locale);
case 'month':
return dateFormat(d, 'MMM', locale);
default:
return label;
}
},
[locale, unit],
);
const renderTooltip = useCallback(
model => {
const { opacity, labelColors, dataPoints } = model.tooltip;
if (!dataPoints?.length || !opacity) {
setTooltip(null);
return;
}
const formats = {
millisecond: 'T',
second: 'pp',
minute: 'p',
hour: 'h:mm aaa - PP',
day: 'PPPP',
week: 'PPPP',
month: 'LLLL yyyy',
quarter: 'qqq',
year: 'yyyy',
};
setTooltip(
<div className={styles.tooltip}>
<div>{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
<div>
<StatusLight color={labelColors?.[0]?.backgroundColor}>
<div className={styles.value}>
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
</div>
</StatusLight>
</div>
</div>,
);
},
[unit],
);
const getOptions = useCallback(() => {
return {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: animationDuration,
resize: {
duration: 0,
},
active: {
duration: 0,
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
external: renderTooltip,
},
},
scales: {
x: {
type: 'time',
stacked: true,
time: {
unit,
},
grid: {
display: false,
},
border: {
color: colors.line,
},
ticks: {
color: colors.text,
autoSkip: false,
maxRotation: 0,
callback: renderXLabel,
},
},
y: {
type: 'linear',
min: 0,
beginAtZero: true,
stacked,
grid: {
color: colors.line,
},
border: {
color: colors.line,
},
ticks: {
color: colors.text,
callback: renderYLabel,
},
},
},
};
}, [animationDuration, renderTooltip, renderXLabel, stacked, colors, unit, locale]);
const createChart = () => {
Chart.defaults.font.family = 'Inter';
const options = getOptions();
chart.current = new Chart(canvas.current, {
type: 'bar',
data: {
datasets,
},
options,
});
onCreate(chart.current);
};
const updateChart = () => {
setTooltip(null);
datasets.forEach((dataset, index) => {
chart.current.data.datasets[index].data = dataset.data;
chart.current.data.datasets[index].label = dataset.label;
});
chart.current.options = getOptions();
onUpdate(chart.current);
chart.current.update();
};
useEffect(() => {
if (datasets) {
if (!chart.current) {
createChart();
} else {
updateChart();
}
}
}, [datasets, unit, theme, animationDuration, locale]);
return (
<>
<div className={classNames(styles.chart, className)}>
{loading && <Loading position="page" icon="dots" />}
<canvas ref={canvas} />
</div>
<Legend chart={chart.current} />
{tooltip && <HoverTooltip tooltip={tooltip} />}
</>
);
}
export default BarChart;

View File

@ -1,32 +0,0 @@
import MetricsTable from './MetricsTable';
import { emptyFilter } from 'lib/filters';
import FilterLink from 'components/common/FilterLink';
import useLocale from 'hooks/useLocale';
import useMessages from 'hooks/useMessages';
export function CitiesTable({ websiteId, ...props }) {
const { locale } = useLocale();
const { formatMessage, labels } = useMessages();
function renderLink({ x }) {
return (
<div className={locale}>
<FilterLink id="city" value={x} />
</div>
);
}
return (
<MetricsTable
{...props}
title={formatMessage(labels.cities)}
type="city"
metric={formatMessage(labels.visitors)}
websiteId={websiteId}
dataFilter={emptyFilter}
renderLabel={renderLink}
/>
);
}
export default CitiesTable;

View File

@ -1,115 +0,0 @@
import { useState } from 'react';
import { Loading } from 'react-basics';
import ErrorMessage from 'components/common/ErrorMessage';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard';
import useMessages from 'hooks/useMessages';
import styles from './MetricsBar.module.css';
export function MetricsBar({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
const [format, setFormat] = useState(true);
const {
query: { url, referrer, os, browser, device, country, region, city },
} = usePageQuery();
const { data, error, isLoading, isFetched } = useQuery(
[
'websites:stats',
{ websiteId, modified, url, referrer, os, browser, device, country, region, city },
],
() =>
get(`/websites/${websiteId}/stats`, {
startAt: +startDate,
endAt: +endDate,
url,
referrer,
os,
browser,
device,
country,
region,
city,
}),
);
const formatFunc = format
? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
: formatNumber;
function handleSetFormat() {
setFormat(state => !state);
}
const { pageviews, uniques, bounces, totaltime } = data || {};
const num = Math.min(data && uniques.value, data && bounces.value);
const diffs = data && {
pageviews: pageviews.value - pageviews.change,
uniques: uniques.value - uniques.change,
bounces: bounces.value - bounces.change,
totaltime: totaltime.value - totaltime.change,
};
return (
<div className={styles.bar} onClick={handleSetFormat}>
{isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
{data && !error && isFetched && (
<>
<MetricCard
className={styles.card}
label={formatMessage(labels.views)}
value={pageviews.value}
change={pageviews.change}
format={formatFunc}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.visitors)}
value={uniques.value}
change={uniques.change}
format={formatFunc}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.bounceRate)}
value={uniques.value ? (num / uniques.value) * 100 : 0}
change={
uniques.value && uniques.change
? (num / uniques.value) * 100 -
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) * 100 || 0
: 0
}
format={n => Number(n).toFixed(0) + '%'}
reverseColors
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.averageVisitTime)}
value={
totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value)
: 0
}
change={
totaltime.value && pageviews.value
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
totaltime.value / (pageviews.value - bounces.value)) *
-1 || 0
: 0
}
format={n => `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
/>
</>
)}
</div>
);
}
export default MetricsBar;

View File

@ -1,64 +0,0 @@
import { useMemo } from 'react';
import { colord } from 'colord';
import BarChart from './BarChart';
import { THEME_COLORS } from 'lib/constants';
import useTheme from 'hooks/useTheme';
import useMessages from 'hooks/useMessages';
import useLocale from 'hooks/useLocale';
export function PageviewsChart({ websiteId, data, unit, records, className, loading, ...props }) {
const { formatMessage, labels } = useMessages();
const [theme] = useTheme();
const { locale } = useLocale();
const colors = useMemo(() => {
const primaryColor = colord(THEME_COLORS[theme].primary);
return {
views: {
hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
backgroundColor: primaryColor.alpha(0.4).toRgbString(),
borderColor: primaryColor.alpha(0.7).toRgbString(),
hoverBorderColor: primaryColor.toRgbString(),
},
visitors: {
hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
backgroundColor: primaryColor.alpha(0.6).toRgbString(),
borderColor: primaryColor.alpha(0.9).toRgbString(),
hoverBorderColor: primaryColor.toRgbString(),
},
};
}, [theme]);
const datasets = useMemo(() => {
if (!data) return [];
return [
{
label: formatMessage(labels.uniqueVisitors),
data: data.sessions,
borderWidth: 1,
...colors.visitors,
},
{
label: formatMessage(labels.pageViews),
data: data.pageviews,
borderWidth: 1,
...colors.views,
},
];
}, [data, locale, colors]);
return (
<BarChart
{...props}
key={websiteId}
className={className}
datasets={datasets}
unit={unit}
records={records}
loading={loading}
/>
);
}
export default PageviewsChart;

View File

@ -1,132 +0,0 @@
import { useMemo } from 'react';
import { Button, Icon, Text, Row, Column } from 'react-basics';
import Link from 'next/link';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/input/DateFilter';
import ErrorMessage from 'components/common/ErrorMessage';
import FilterTags from 'components/metrics/FilterTags';
import RefreshButton from 'components/input/RefreshButton';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength } from 'lib/date';
import Icons from 'components/icons';
import useSticky from 'hooks/useSticky';
import useMessages from 'hooks/useMessages';
import styles from './WebsiteChart.module.css';
import useLocale from 'hooks/useLocale';
export function WebsiteChart({
websiteId,
name,
domain,
stickyHeader = false,
showChart = true,
showDetailsButton = false,
onDataLoad = () => {},
}) {
const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone();
const {
query: { url, referrer, os, browser, device, country, region, city, title },
} = usePageQuery();
const { get, useQuery } = useApi();
const { ref, isSticky } = useSticky({ enabled: stickyHeader });
const { data, isLoading, error } = useQuery(
[
'websites:pageviews',
{ websiteId, modified, url, referrer, os, browser, device, country, region, city, title },
],
() =>
get(`/websites/${websiteId}/pageviews`, {
startAt: +startDate,
endAt: +endDate,
unit,
timezone,
url,
referrer,
os,
browser,
device,
country,
region,
city,
title,
}),
{ onSuccess: onDataLoad },
);
const chartData = useMemo(() => {
if (data) {
return {
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
sessions: getDateArray(data.sessions, startDate, endDate, unit),
};
}
return { pageviews: [], sessions: [] };
}, [data, modified]);
const { dir } = useLocale();
return (
<>
<WebsiteHeader websiteId={websiteId} name={name} domain={domain}>
{showDetailsButton && (
<Link href={`/websites/${websiteId}`}>
<Button variant="primary">
<Text>{formatMessage(labels.viewDetails)}</Text>
<Icon>
<Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight />
</Icon>
</Icon>
</Button>
</Link>
)}
</WebsiteHeader>
<FilterTags
websiteId={websiteId}
params={{ url, referrer, os, browser, device, country, region, city, title }}
/>
<Row
ref={ref}
className={classNames(styles.header, {
[styles.sticky]: stickyHeader,
[styles.isSticky]: isSticky,
})}
>
<Column defaultSize={12} xl={8}>
<MetricsBar websiteId={websiteId} />
</Column>
<Column defaultSize={12} xl={4}>
<div className={styles.actions}>
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
<DateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
</div>
</Column>
</Row>
<Row>
<Column className={styles.chart}>
{error && <ErrorMessage />}
{showChart && (
<PageviewsChart
websiteId={websiteId}
data={chartData}
unit={unit}
records={getDateLength(startDate, endDate, unit)}
loading={isLoading}
/>
)}
</Column>
</Row>
</>
);
}
export default WebsiteChart;

View File

@ -1,21 +0,0 @@
import { Row, Column, Text } from 'react-basics';
import Favicon from 'components/common/Favicon';
import ActiveUsers from './ActiveUsers';
import styles from './WebsiteHeader.module.css';
export function WebsiteHeader({ websiteId, name, domain, children }) {
return (
<Row className={styles.header} justifyContent="center">
<Column className={styles.title} variant="two">
<Favicon domain={domain} />
<Text>{name}</Text>
</Column>
<Column className={styles.info} variant="two">
<ActiveUsers websiteId={websiteId} />
{children}
</Column>
</Row>
);
}
export default WebsiteHeader;

View File

@ -1,19 +0,0 @@
.title {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
font-size: 24px;
font-weight: 700;
overflow: hidden;
height: 100px;
}
.info {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 30px;
min-height: 0;
}

View File

@ -1,46 +0,0 @@
import { Menu, Icon, Text, PopupTrigger, Popup, Item, Button } from 'react-basics';
import Icons from 'components/icons';
import { saveDashboard } from 'store/dashboard';
import useMessages from 'hooks/useMessages';
export function DashboardSettingsButton() {
const { formatMessage, labels } = useMessages();
const menuOptions = [
{
label: formatMessage(labels.toggleCharts),
value: 'charts',
},
{
label: formatMessage(labels.editDashboard),
value: 'order',
},
];
function handleSelect(value) {
if (value === 'charts') {
saveDashboard(state => ({ showCharts: !state.showCharts }));
}
if (value === 'order') {
saveDashboard({ editing: true });
}
}
return (
<PopupTrigger>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
<Popup alignment="end">
<Menu variant="popup" items={menuOptions} onSelect={handleSelect}>
{({ label, value }) => <Item key={value}>{label}</Item>}
</Menu>
</Popup>
</PopupTrigger>
);
}
export default DashboardSettingsButton;

View File

@ -1,27 +0,0 @@
import { useCallback } from 'react';
import DataTable from 'components/metrics/DataTable';
import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames';
import useMessages from 'hooks/useMessages';
export function RealtimeCountries({ data }) {
const { formatMessage, labels } = useMessages();
const { locale } = useLocale();
const countryNames = useCountryNames(locale);
const renderCountryName = useCallback(
({ x }) => <span className={locale}>{countryNames[x]}</span>,
[countryNames, locale],
);
return (
<DataTable
title={formatMessage(labels.countries)}
metric={formatMessage(labels.visitors)}
data={data}
renderLabel={renderCountryName}
/>
);
}
export default RealtimeCountries;

View File

@ -1,31 +0,0 @@
import { Loading, useToast } from 'react-basics';
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
export function TeamMembers({ teamId, readOnly }) {
const { toast, showToast } = useToast();
const { get, useQuery } = useApi();
const { formatMessage, messages } = useMessages();
const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () =>
get(`/teams/${teamId}/users`),
);
if (isLoading) {
return <Loading icon="dots" style={{ minHeight: 300 }} />;
}
const handleSave = async () => {
await refetch();
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
return (
<>
{toast}
<TeamMembersTable onSave={handleSave} data={data} readOnly={readOnly} />
</>
);
}
export default TeamMembers;

View File

@ -1,26 +0,0 @@
import { Loading } from 'react-basics';
import useApi from 'hooks/useApi';
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
import useMessages from 'hooks/useMessages';
export function UserWebsites({ userId }) {
const { formatMessage, messages } = useMessages();
const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['user:websites', userId], () =>
get(`/users/${userId}/websites`),
);
const hasData = data && data.length !== 0;
if (isLoading) {
return <Loading icon="dots" style={{ minHeight: 300 }} />;
}
return (
<div>
{hasData && <WebsitesTable data={data} />}
{!hasData && formatMessage(messages.noDataAvailable)}
</div>
);
}
export default UserWebsites;

View File

@ -1,56 +0,0 @@
import { Button, Icon, Text, Modal, ModalTrigger, useToast, Icons } from 'react-basics';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser';
import useMessages from 'hooks/useMessages';
export function WebsitesList() {
const { formatMessage, labels, messages } = useMessages();
const { user } = useUser();
const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery(
['websites', user?.id],
() => get(`/users/${user?.id}/websites`),
{ enabled: !!user },
);
const { toast, showToast } = useToast();
const hasData = data && data.length !== 0;
const handleSave = async () => {
await refetch();
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
const addButton = (
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal title={formatMessage(labels.addWebsite)}>
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
);
return (
<Page loading={isLoading} error={error}>
{toast}
<PageHeader title={formatMessage(labels.websites)}>{addButton}</PageHeader>
{hasData && <WebsitesTable data={data} />}
{!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
{addButton}
</EmptyPlaceholder>
)}
</Page>
);
}
export default WebsitesList;

View File

@ -1,47 +0,0 @@
import Link from 'next/link';
import { Button, Text, Icon, Icons } from 'react-basics';
import SettingsTable from 'components/common/SettingsTable';
import useMessages from 'hooks/useMessages';
import useConfig from 'hooks/useConfig';
export function WebsitesTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
const { openExternal } = useConfig();
const columns = [
{ name: 'name', label: formatMessage(labels.name) },
{ name: 'domain', label: formatMessage(labels.domain) },
{ name: 'action', label: ' ' },
];
return (
<SettingsTable columns={columns} data={data}>
{row => {
const { id } = row;
return (
<>
<Link href={`/settings/websites/${id}`}>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
<Link href={`/websites/${id}`} target={openExternal ? '_blank' : null}>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
</>
);
}}
</SettingsTable>
);
}
export default WebsitesTable;

View File

@ -1,35 +0,0 @@
import { useMemo } from 'react';
import { firstBy } from 'thenby';
import WebsiteChart from 'components/metrics/WebsiteChart';
import useDashboard from 'store/dashboard';
import styles from './WebsiteList.module.css';
export default function WebsiteChartList({ websites, showCharts, limit }) {
const { websiteOrder } = useDashboard();
const ordered = useMemo(
() =>
websites
.map(website => ({ ...website, order: websiteOrder.indexOf(website.id) || 0 }))
.sort(firstBy('order')),
[websites, websiteOrder],
);
return (
<div>
{ordered.map(({ id, name, domain }, index) => {
return index < limit ? (
<div key={id} className={styles.website}>
<WebsiteChart
websiteId={id}
name={name}
domain={domain}
showChart={showCharts}
showDetailsButton={true}
/>
</div>
) : null;
})}
</div>
);
}

View File

@ -1,47 +0,0 @@
import { useState } from 'react';
import { Loading } from 'react-basics';
import Page from 'components/layout/Page';
import WebsiteChart from 'components/metrics/WebsiteChart';
import useApi from 'hooks/useApi';
import usePageQuery from 'hooks/usePageQuery';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import WebsiteTableView from './WebsiteTableView';
import WebsiteMenuView from './WebsiteMenuView';
export default function WebsiteDetails({ websiteId }) {
const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites', websiteId], () =>
get(`/websites/${websiteId}`),
);
const [chartLoaded, setChartLoaded] = useState(false);
const {
query: { view },
} = usePageQuery();
function handleDataLoad() {
if (!chartLoaded) {
setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION);
}
}
return (
<Page loading={isLoading} error={error}>
<WebsiteChart
websiteId={websiteId}
name={data?.name}
domain={data?.domain}
onDataLoad={handleDataLoad}
showLink={false}
stickyHeader={true}
/>
{!chartLoaded && <Loading icon="dots" style={{ minHeight: 300 }} />}
{chartLoaded && (
<>
{!view && <WebsiteTableView websiteId={websiteId} />}
{view && <WebsiteMenuView websiteId={websiteId} />}
</>
)}
</Page>
);
}

View File

@ -1,31 +0,0 @@
.chart {
margin-bottom: 30px;
}
.view {
border-top: 1px solid var(--base300);
}
.menu {
font-size: var(--font-size-sm);
}
.content {
min-height: 600px;
padding: 20px 0;
}
.backButton {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
}
.backButton svg {
transform: rotate(180deg);
}
.hidden {
display: none;
}

View File

@ -0,0 +1,18 @@
-- edit event_data values
ALTER TABLE "event_data" RENAME COLUMN "event_date_value" TO "date_value";
ALTER TABLE "event_data" RENAME COLUMN "event_numeric_value" TO "number_value";
ALTER TABLE "event_data" RENAME COLUMN "event_string_value" TO "string_value";
ALTER TABLE "event_data" RENAME COLUMN "event_data_type" TO "data_type";
-- add job_id
ALTER TABLE "website_event" ADD COLUMN "job_id" UUID AFTER "created_at";
ALTER TABLE "event_data" ADD COLUMN "job_id" UUID AFTER "created_at";
-- update event_data string
alter table umami.event_data
update string_value = number_value
where data_type = 2
alter table umami.event_data
update string_value = replaceOne(concat(CAST(toDateTime(date_value, 'UTC'), 'String'),'Z'), ' ', 'T')
where data_type = 4

View File

@ -6,7 +6,7 @@ CREATE TABLE umami.website_event
website_id UUID,
session_id UUID,
event_id UUID,
--session
--sessions
hostname LowCardinality(String),
browser LowCardinality(String),
os LowCardinality(String),
@ -17,17 +17,18 @@ CREATE TABLE umami.website_event
subdivision1 LowCardinality(String),
subdivision2 LowCardinality(String),
city String,
--pageview
--pageviews
url_path String,
url_query String,
referrer_path String,
referrer_query String,
referrer_domain String,
page_title String,
--event
--events
event_type UInt32,
event_name String,
created_at DateTime('UTC')
created_at DateTime('UTC'),
job_id UUID
)
engine = MergeTree
ORDER BY (website_id, session_id, created_at)
@ -37,7 +38,7 @@ CREATE TABLE umami.website_event_queue (
website_id UUID,
session_id UUID,
event_id UUID,
--session
--sessions
hostname LowCardinality(String),
browser LowCardinality(String),
os LowCardinality(String),
@ -48,25 +49,28 @@ CREATE TABLE umami.website_event_queue (
subdivision1 LowCardinality(String),
subdivision2 LowCardinality(String),
city String,
--pageview
--pageviews
url_path String,
url_query String,
referrer_path String,
referrer_query String,
referrer_domain String,
page_title String,
--event
--events
event_type UInt32,
event_name String,
created_at DateTime('UTC')
created_at DateTime('UTC'),
--virtual columns
_error String,
_raw_message String
)
ENGINE = Kafka
SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list
kafka_topic_list = 'event',
kafka_topic_list = 'events',
kafka_group_name = 'event_consumer_group',
kafka_format = 'JSONEachRow',
kafka_max_block_size = 1048576,
kafka_skip_broken_messages = 100;
kafka_handle_error_mode = 'stream';
CREATE MATERIALIZED VIEW umami.website_event_queue_mv TO umami.website_event AS
SELECT website_id,
@ -93,6 +97,19 @@ SELECT website_id,
created_at
FROM umami.website_event_queue;
CREATE MATERIALIZED VIEW umami.website_event_errors_mv
(
error String,
raw String
)
ENGINE = MergeTree
ORDER BY (error, raw)
SETTINGS index_granularity = 8192 AS
SELECT _error AS error,
_raw_message AS raw
FROM umami.website_event_queue
WHERE length(_error) > 0;
CREATE TABLE umami.event_data
(
website_id UUID,
@ -101,11 +118,12 @@ CREATE TABLE umami.event_data
url_path String,
event_name String,
event_key String,
event_string_value Nullable(String),
event_numeric_value Nullable(Decimal64(4)), --922337203685477.5625
event_date_value Nullable(DateTime('UTC')),
event_data_type UInt32,
created_at DateTime('UTC')
string_value Nullable(String),
number_value Nullable(Decimal64(4)), --922337203685477.5625
date_value Nullable(DateTime('UTC')),
data_type UInt32,
created_at DateTime('UTC'),
job_id UUID
)
engine = MergeTree
ORDER BY (website_id, event_id, event_key, created_at)
@ -118,11 +136,14 @@ CREATE TABLE umami.event_data_queue (
url_path String,
event_name String,
event_key String,
event_string_value Nullable(String),
event_numeric_value Nullable(Decimal64(4)), --922337203685477.5625
event_date_value Nullable(DateTime('UTC')),
event_data_type UInt32,
created_at DateTime('UTC')
string_value Nullable(String),
number_value Nullable(Decimal64(4)), --922337203685477.5625
date_value Nullable(DateTime('UTC')),
data_type UInt32,
created_at DateTime('UTC'),
--virtual columns
_error String,
_raw_message String
)
ENGINE = Kafka
SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list
@ -130,7 +151,7 @@ SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input bro
kafka_group_name = 'event_data_consumer_group',
kafka_format = 'JSONEachRow',
kafka_max_block_size = 1048576,
kafka_skip_broken_messages = 100;
kafka_handle_error_mode = 'stream';
CREATE MATERIALIZED VIEW umami.event_data_queue_mv TO umami.event_data AS
SELECT website_id,
@ -139,9 +160,22 @@ SELECT website_id,
url_path,
event_name,
event_key,
event_string_value,
event_numeric_value,
event_date_value,
event_data_type,
string_value,
number_value,
date_value,
data_type,
created_at
FROM umami.event_data_queue;
CREATE MATERIALIZED VIEW umami.event_data_errors_mv
(
error String,
raw String
)
ENGINE = MergeTree
ORDER BY (error, raw)
SETTINGS index_granularity = 8192 AS
SELECT _error AS error,
_raw_message AS raw
FROM umami.event_data_queue
WHERE length(_error) > 0;

View File

@ -0,0 +1,53 @@
-- AlterTable
ALTER TABLE `event_data` RENAME COLUMN `event_data_type` TO `data_type`;
ALTER TABLE `event_data` RENAME COLUMN `event_date_value` TO `date_value`;
ALTER TABLE `event_data` RENAME COLUMN `event_id` TO `event_data_id`;
ALTER TABLE `event_data` RENAME COLUMN `event_numeric_value` TO `number_value`;
ALTER TABLE `event_data` RENAME COLUMN `event_string_value` TO `string_value`;
-- CreateTable
CREATE TABLE `session_data` (
`session_data_id` VARCHAR(36) NOT NULL,
`website_id` VARCHAR(36) NOT NULL,
`session_id` VARCHAR(36) NOT NULL,
`event_key` VARCHAR(500) NOT NULL,
`string_value` VARCHAR(500) NULL,
`number_value` DECIMAL(19, 4) NULL,
`date_value` TIMESTAMP(0) NULL,
`data_type` INTEGER UNSIGNED NOT NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
INDEX `session_data_created_at_idx`(`created_at`),
INDEX `session_data_website_id_idx`(`website_id`),
INDEX `session_data_session_id_idx`(`session_id`),
PRIMARY KEY (`session_data_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `report` (
`report_id` VARCHAR(36) NOT NULL,
`user_id` VARCHAR(36) NOT NULL,
`website_id` VARCHAR(36) NOT NULL,
`type` VARCHAR(200) NOT NULL,
`name` VARCHAR(200) NOT NULL,
`description` VARCHAR(500) NOT NULL,
`parameters` VARCHAR(6000) NOT NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`updated_at` TIMESTAMP(0) NULL,
UNIQUE INDEX `report_report_id_key`(`report_id`),
INDEX `report_user_id_idx`(`user_id`),
INDEX `report_website_id_idx`(`website_id`),
INDEX `report_type_idx`(`type`),
INDEX `report_name_idx`(`name`),
PRIMARY KEY (`report_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- EventData migration
UPDATE event_data
SET string_value = number_value
WHERE data_type = 2;
UPDATE event_data
SET string_value = CONCAT(REPLACE(DATE_FORMAT(date_value, '%Y-%m-%d %T'), ' ', 'T'), 'Z')
WHERE data_type = 4;

View File

@ -0,0 +1,50 @@
-- CreateIndex
CREATE INDEX `event_data_website_id_created_at_idx` ON `event_data`(`website_id`, `created_at`);
-- CreateIndex
CREATE INDEX `event_data_website_id_created_at_event_key_idx` ON `event_data`(`website_id`, `created_at`, `event_key`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_idx` ON `session`(`website_id`, `created_at`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_hostname_idx` ON `session`(`website_id`, `created_at`, `hostname`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_browser_idx` ON `session`(`website_id`, `created_at`, `browser`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_os_idx` ON `session`(`website_id`, `created_at`, `os`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_device_idx` ON `session`(`website_id`, `created_at`, `device`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_screen_idx` ON `session`(`website_id`, `created_at`, `screen`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_language_idx` ON `session`(`website_id`, `created_at`, `language`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_country_idx` ON `session`(`website_id`, `created_at`, `country`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_subdivision1_idx` ON `session`(`website_id`, `created_at`, `subdivision1`);
-- CreateIndex
CREATE INDEX `session_website_id_created_at_city_idx` ON `session`(`website_id`, `created_at`, `city`);
-- CreateIndex
CREATE INDEX `website_event_website_id_created_at_url_path_idx` ON `website_event`(`website_id`, `created_at`, `url_path`);
-- CreateIndex
CREATE INDEX `website_event_website_id_created_at_url_query_idx` ON `website_event`(`website_id`, `created_at`, `url_query`);
-- CreateIndex
CREATE INDEX `website_event_website_id_created_at_referrer_domain_idx` ON `website_event`(`website_id`, `created_at`, `referrer_domain`);
-- CreateIndex
CREATE INDEX `website_event_website_id_created_at_page_title_idx` ON `website_event`(`website_id`, `created_at`, `page_title`);
-- CreateIndex
CREATE INDEX `website_event_website_id_created_at_event_name_idx` ON `website_event`(`website_id`, `created_at`, `event_name`);

View File

@ -14,11 +14,12 @@ model User {
password String @db.VarChar(60)
role String @map("role") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
website Website[]
teamUser TeamUser[]
report Report[]
@@map("user")
}
@ -39,9 +40,20 @@ model Session {
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
websiteEvent WebsiteEvent[]
sessionData SessionData[]
@@index([createdAt])
@@index([websiteId])
@@index([websiteId, createdAt])
@@index([websiteId, createdAt, hostname])
@@index([websiteId, createdAt, browser])
@@index([websiteId, createdAt, os])
@@index([websiteId, createdAt, device])
@@index([websiteId, createdAt, screen])
@@index([websiteId, createdAt, language])
@@index([websiteId, createdAt, country])
@@index([websiteId, createdAt, subdivision1])
@@index([websiteId, createdAt, city])
@@map("session")
}
@ -53,12 +65,14 @@ model Website {
resetAt DateTime? @map("reset_at") @db.Timestamp(0)
userId String? @map("user_id") @db.VarChar(36)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
user User? @relation(fields: [userId], references: [id])
teamWebsite TeamWebsite[]
eventData EventData[]
report Report[]
sessionData SessionData[]
@@index([userId])
@@index([createdAt])
@ -87,19 +101,24 @@ model WebsiteEvent {
@@index([sessionId])
@@index([websiteId])
@@index([websiteId, createdAt])
@@index([websiteId, createdAt, urlPath])
@@index([websiteId, createdAt, urlQuery])
@@index([websiteId, createdAt, referrerDomain])
@@index([websiteId, createdAt, pageTitle])
@@index([websiteId, createdAt, eventName])
@@index([websiteId, sessionId, createdAt])
@@map("website_event")
}
model EventData {
id String @id() @map("event_id") @db.VarChar(36)
websiteEventId String @map("website_event_id") @db.VarChar(36)
id String @id() @map("event_data_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
websiteEventId String @map("website_event_id") @db.VarChar(36)
eventKey String @map("event_key") @db.VarChar(500)
eventStringValue String? @map("event_string_value") @db.VarChar(500)
eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4)
eventDateValue DateTime? @map("event_date_value") @db.Timestamp(0)
eventDataType Int @map("event_data_type") @db.UnsignedInt
stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamp(0)
dataType Int @map("data_type") @db.UnsignedInt
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
website Website @relation(fields: [websiteId], references: [id])
@ -109,15 +128,37 @@ model EventData {
@@index([websiteId])
@@index([websiteEventId])
@@index([websiteId, websiteEventId, createdAt])
@@index([websiteId, createdAt])
@@index([websiteId, createdAt, eventKey])
@@map("event_data")
}
model SessionData {
id String @id() @map("session_data_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
sessionId String @map("session_id") @db.VarChar(36)
eventKey String @map("event_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamp(0)
dataType Int @map("data_type") @db.UnsignedInt
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
website Website @relation(fields: [websiteId], references: [id])
session Session @relation(fields: [sessionId], references: [id])
@@index([createdAt])
@@index([websiteId])
@@index([sessionId])
@@map("session_data")
}
model Team {
id String @id() @unique() @map("team_id") @db.VarChar(36)
name String @db.VarChar(50)
accessCode String? @unique @map("access_code") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
teamUser TeamUser[]
teamWebsite TeamWebsite[]
@ -132,7 +173,7 @@ model TeamUser {
userId String @map("user_id") @db.VarChar(36)
role String @map("role") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id])
@ -155,3 +196,24 @@ model TeamWebsite {
@@index([websiteId])
@@map("team_website")
}
model Report {
id String @id() @unique() @map("report_id") @db.VarChar(36)
userId String @map("user_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
type String @map("type") @db.VarChar(200)
name String @map("name") @db.VarChar(200)
description String @map("description") @db.VarChar(500)
parameters String @map("parameters") @db.VarChar(6000)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
user User @relation(fields: [userId], references: [id])
website Website @relation(fields: [websiteId], references: [id])
@@index([userId])
@@index([websiteId])
@@index([type])
@@index([name])
@@map("report")
}

View File

@ -0,0 +1,70 @@
-- AlterTable
ALTER TABLE "event_data" RENAME COLUMN "event_data_type" TO "data_type";
ALTER TABLE "event_data" RENAME COLUMN "event_date_value" TO "date_value";
ALTER TABLE "event_data" RENAME COLUMN "event_id" TO "event_data_id";
ALTER TABLE "event_data" RENAME COLUMN "event_numeric_value" TO "number_value";
ALTER TABLE "event_data" RENAME COLUMN "event_string_value" TO "string_value";
-- CreateTable
CREATE TABLE "session_data" (
"session_data_id" UUID NOT NULL,
"website_id" UUID NOT NULL,
"session_id" UUID NOT NULL,
"session_key" VARCHAR(500) NOT NULL,
"string_value" VARCHAR(500),
"number_value" DECIMAL(19,4),
"date_value" TIMESTAMPTZ(6),
"data_type" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "session_data_pkey" PRIMARY KEY ("session_data_id")
);
-- CreateTable
CREATE TABLE "report" (
"report_id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"website_id" UUID NOT NULL,
"type" VARCHAR(200) NOT NULL,
"name" VARCHAR(200) NOT NULL,
"description" VARCHAR(500) NOT NULL,
"parameters" VARCHAR(6000) NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6),
CONSTRAINT "report_pkey" PRIMARY KEY ("report_id")
);
-- CreateIndex
CREATE INDEX "session_data_created_at_idx" ON "session_data"("created_at");
-- CreateIndex
CREATE INDEX "session_data_website_id_idx" ON "session_data"("website_id");
-- CreateIndex
CREATE INDEX "session_data_session_id_idx" ON "session_data"("session_id");
-- CreateIndex
CREATE UNIQUE INDEX "report_report_id_key" ON "report"("report_id");
-- CreateIndex
CREATE INDEX "report_user_id_idx" ON "report"("user_id");
-- CreateIndex
CREATE INDEX "report_website_id_idx" ON "report"("website_id");
-- CreateIndex
CREATE INDEX "report_type_idx" ON "report"("type");
-- CreateIndex
CREATE INDEX "report_name_idx" ON "report"("name");
-- EventData migration
UPDATE "event_data"
SET string_value = number_value
WHERE data_type = 2;
UPDATE "event_data"
SET string_value = CONCAT(REPLACE(TO_CHAR(date_value, 'YYYY-MM-DD HH24:MI:SS'), ' ', 'T'), 'Z')
WHERE data_type = 4;

View File

@ -0,0 +1,50 @@
-- CreateIndex
CREATE INDEX "event_data_website_id_created_at_idx" ON "event_data"("website_id", "created_at");
-- CreateIndex
CREATE INDEX "event_data_website_id_created_at_event_key_idx" ON "event_data"("website_id", "created_at", "event_key");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_idx" ON "session"("website_id", "created_at");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_hostname_idx" ON "session"("website_id", "created_at", "hostname");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_browser_idx" ON "session"("website_id", "created_at", "browser");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_os_idx" ON "session"("website_id", "created_at", "os");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_device_idx" ON "session"("website_id", "created_at", "device");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_screen_idx" ON "session"("website_id", "created_at", "screen");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_language_idx" ON "session"("website_id", "created_at", "language");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_country_idx" ON "session"("website_id", "created_at", "country");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_subdivision1_idx" ON "session"("website_id", "created_at", "subdivision1");
-- CreateIndex
CREATE INDEX "session_website_id_created_at_city_idx" ON "session"("website_id", "created_at", "city");
-- CreateIndex
CREATE INDEX "website_event_website_id_created_at_url_path_idx" ON "website_event"("website_id", "created_at", "url_path");
-- CreateIndex
CREATE INDEX "website_event_website_id_created_at_url_query_idx" ON "website_event"("website_id", "created_at", "url_query");
-- CreateIndex
CREATE INDEX "website_event_website_id_created_at_referrer_domain_idx" ON "website_event"("website_id", "created_at", "referrer_domain");
-- CreateIndex
CREATE INDEX "website_event_website_id_created_at_page_title_idx" ON "website_event"("website_id", "created_at", "page_title");
-- CreateIndex
CREATE INDEX "website_event_website_id_created_at_event_name_idx" ON "website_event"("website_id", "created_at", "event_name");

View File

@ -14,11 +14,12 @@ model User {
password String @db.VarChar(60)
role String @map("role") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
website Website[]
teamUser TeamUser[]
report Report[]
@@map("user")
}
@ -39,9 +40,20 @@ model Session {
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
websiteEvent WebsiteEvent[]
sessionData SessionData[]
@@index([createdAt])
@@index([websiteId])
@@index([websiteId, createdAt])
@@index([websiteId, createdAt, hostname])
@@index([websiteId, createdAt, browser])
@@index([websiteId, createdAt, os])
@@index([websiteId, createdAt, device])
@@index([websiteId, createdAt, screen])
@@index([websiteId, createdAt, language])
@@index([websiteId, createdAt, country])
@@index([websiteId, createdAt, subdivision1])
@@index([websiteId, createdAt, city])
@@map("session")
}
@ -53,12 +65,14 @@ model Website {
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
userId String? @map("user_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
user User? @relation(fields: [userId], references: [id])
teamWebsite TeamWebsite[]
eventData EventData[]
report Report[]
sessionData SessionData[]
@@index([userId])
@@index([createdAt])
@ -87,19 +101,24 @@ model WebsiteEvent {
@@index([sessionId])
@@index([websiteId])
@@index([websiteId, createdAt])
@@index([websiteId, createdAt, urlPath])
@@index([websiteId, createdAt, urlQuery])
@@index([websiteId, createdAt, referrerDomain])
@@index([websiteId, createdAt, pageTitle])
@@index([websiteId, createdAt, eventName])
@@index([websiteId, sessionId, createdAt])
@@map("website_event")
}
model EventData {
id String @id() @map("event_id") @db.Uuid
id String @id() @map("event_data_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
websiteEventId String @map("website_event_id") @db.Uuid
eventKey String @map("event_key") @db.VarChar(500)
eventStringValue String? @map("event_string_value") @db.VarChar(500)
eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4)
eventDateValue DateTime? @map("event_date_value") @db.Timestamptz(6)
eventDataType Int @map("event_data_type") @db.Integer
stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamptz(6)
dataType Int @map("data_type") @db.Integer
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
website Website @relation(fields: [websiteId], references: [id])
@ -108,15 +127,38 @@ model EventData {
@@index([createdAt])
@@index([websiteId])
@@index([websiteEventId])
@@index([websiteId, createdAt])
@@index([websiteId, createdAt, eventKey])
@@map("event_data")
}
model SessionData {
id String @id() @map("session_data_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid
sessionKey String @map("session_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamptz(6)
dataType Int @map("data_type") @db.Integer
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
deletedAt DateTime? @default(now()) @map("deleted_at") @db.Timestamptz(6)
website Website @relation(fields: [websiteId], references: [id])
session Session @relation(fields: [sessionId], references: [id])
@@index([createdAt])
@@index([websiteId])
@@index([sessionId])
@@map("session_data")
}
model Team {
id String @id() @unique() @map("team_id") @db.Uuid
name String @db.VarChar(50)
accessCode String? @unique @map("access_code") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
teamUser TeamUser[]
teamWebsite TeamWebsite[]
@ -131,7 +173,7 @@ model TeamUser {
userId String @map("user_id") @db.Uuid
role String @map("role") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id])
@ -154,3 +196,24 @@ model TeamWebsite {
@@index([websiteId])
@@map("team_website")
}
model Report {
id String @id() @unique() @map("report_id") @db.Uuid
userId String @map("user_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
type String @map("type") @db.VarChar(200)
name String @map("name") @db.VarChar(200)
description String @map("description") @db.VarChar(500)
parameters String @map("parameters") @db.VarChar(6000)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
user User @relation(fields: [userId], references: [id])
website Website @relation(fields: [websiteId], references: [id])
@@index([userId])
@@index([websiteId])
@@index([type])
@@index([name])
@@map("report")
}

View File

@ -10,7 +10,8 @@ services:
DATABASE_TYPE: postgresql
APP_SECRET: replace-me-with-a-random-string
depends_on:
- db
db:
condition: service_healthy
restart: always
db:
image: postgres:15-alpine
@ -19,8 +20,12 @@ services:
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- ./sql/schema.postgresql.sql:/docker-entrypoint-initdb.d/schema.postgresql.sql:ro
- umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
umami-db-data:

View File

@ -1,25 +0,0 @@
import { parseDateRange } from 'lib/date';
import { setItem } from 'next-basics';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
import useLocale from './useLocale';
import websiteStore, { setWebsiteDateRange } from 'store/websites';
import appStore, { setDateRange } from 'store/app';
export default function useDateRange(websiteId) {
const { locale } = useLocale();
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
const defaultConfig = DEFAULT_DATE_RANGE;
const globalConfig = appStore(state => state.dateRange);
const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
function saveDateRange(value) {
if (websiteId) {
setWebsiteDateRange(websiteId, value);
} else {
setItem(DATE_RANGE_CONFIG, value);
setDateRange(value);
}
}
return [dateRange, saveDateRange];
}

View File

@ -1,36 +0,0 @@
import { useEffect } from 'react';
import useStore, { setTheme } from 'store/app';
import { getItem, setItem } from 'next-basics';
import { THEME_CONFIG } from 'lib/constants';
const selector = state => state.theme;
export default function useTheme() {
const defaultTheme =
typeof window !== 'undefined'
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
? 'dark'
: 'light'
: 'light';
const theme = useStore(selector) || getItem(THEME_CONFIG) || defaultTheme;
function saveTheme(value) {
setItem(THEME_CONFIG, value);
setTheme(value);
}
useEffect(() => {
document.body.setAttribute('data-theme', theme);
}, [theme]);
useEffect(() => {
const url = new URL(window?.location?.href);
const theme = url.searchParams.get('theme');
if (['light', 'dark'].includes(theme)) {
saveTheme(theme);
}
}, []);
return [theme, saveTheme];
}

View File

@ -1,5 +1,5 @@
{
"compilerOptions": {
"baseUrl": "."
"baseUrl": "./src"
}
}

View File

@ -1,16 +1,32 @@
{
"cs-CZ": ["label.reset", "metrics.device.tablet"],
"de-DE": [
"label.administrator",
"label.name",
"de-CH": [
"label.admin",
"label.analytics",
"label.desktop",
"label.details",
"label.domain",
"label.theme",
"metrics.device.desktop",
"metrics.device.laptop",
"metrics.device.tablet",
"metrics.referrers",
"metrics.utm",
"metrics.utm_medium"
"label.laptop",
"label.tablet",
"label.name",
"label.sessions",
"label.team",
"label.team-id",
"label.teams"
],
"de-DE": [
"label.admin",
"label.analytics",
"label.desktop",
"label.details",
"label.domain",
"label.laptop",
"label.tablet",
"label.name",
"label.sessions",
"label.team",
"label.team-id",
"label.teams"
],
"en-GB": "*",
"fr-FR": ["metrics.actions", "metrics.pages"],
@ -22,12 +38,15 @@
],
"nb-NO": ["label.administrator", "label.dashboard"],
"nl-NL": [
"label.administrator",
"label.websites",
"metrics.browsers",
"metrics.device.desktop",
"metrics.device.laptop",
"metrics.device.tablet"
"label.analytics",
"label.browsers",
"label.laptop",
"label.tablet",
"label.team",
"label.team-id",
"label.teams",
"label.website-id",
"label.websites"
],
"it-IT": [
"label.password",
@ -37,9 +56,5 @@
"metrics.device.tablet",
"metrics.filter.raw"
],
"pt-PT": [
"label.websites",
"metrics.device.desktop",
"metrics.device.tablet"
]
"pt-PT": ["label.websites", "metrics.device.desktop", "metrics.device.tablet"]
}

View File

@ -1,120 +0,0 @@
{
"label.accounts": "Accounts",
"label.add-account": "Add account",
"label.add-column": "Add column",
"label.add-filter": "Add filter",
"label.add-website": "Add website",
"label.administrator": "Administrator",
"label.all": "All",
"label.all-time": "All time",
"label.all-websites": "All websites",
"label.back": "Back",
"label.cancel": "Cancel",
"label.change-password": "Change password",
"label.confirm-password": "Confirm password",
"label.copy-to-clipboard": "Copy to clipboard",
"label.current-password": "Current password",
"label.custom-range": "Custom range",
"label.dashboard": "Dashboard",
"label.date-range": "Date range",
"label.default-date-range": "Default date range",
"label.delete": "Delete",
"label.delete-account": "Delete account",
"label.delete-website": "Delete website",
"label.dismiss": "Dismiss",
"label.domain": "Domain",
"label.edit": "Edit",
"label.edit-account": "Edit account",
"label.edit-website": "Edit website",
"label.enable-share-url": "Enable share URL",
"label.event-data": "Event Data",
"label.field-name": "Field Name",
"label.invalid": "Invalid",
"label.invalid-domain": "Invalid domain",
"label.language": "Language",
"label.last-days": "Last {x} days",
"label.last-hours": "Last {x} hours",
"label.logged-in-as": "Logged in as {username}",
"label.login": "Login",
"label.logout": "Logout",
"label.more": "More",
"label.name": "Name",
"label.new-password": "New password",
"label.none": "None",
"label.owner": "Owner",
"label.password": "Password",
"label.passwords-dont-match": "Passwords don't match",
"label.profile": "Profile",
"label.realtime": "Realtime",
"label.realtime-logs": "Realtime logs",
"label.refresh": "Refresh",
"label.required": "Required",
"label.reset": "Reset",
"label.reset-website": "Reset statistics",
"label.save": "Save",
"label.search": "Search",
"label.settings": "Settings",
"label.share-url": "Share URL",
"label.single-day": "Single day",
"label.theme": "Theme",
"label.this-month": "This month",
"label.this-week": "This week",
"label.this-year": "This year",
"label.timezone": "Timezone",
"label.today": "Today",
"label.tracking-code": "Tracking code",
"label.type": "Type",
"label.unknown": "Unknown",
"label.username": "Username",
"label.value": "Value",
"label.view-details": "View details",
"label.websites": "Websites",
"label.yesterday": "Yesterday",
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
"message.confirm-delete": "Are you sure you want to delete {target}?",
"message.confirm-reset": "Are you sure you want to reset {target}'s statistics?",
"message.copied": "Copied!",
"message.delete-warning": "All associated data will be deleted as well.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Something went wrong.",
"message.get-share-url": "Get share URL",
"message.get-tracking-code": "Get tracking code",
"message.go-to-settings": "Go to settings",
"message.incorrect-username-password": "Incorrect username/password.",
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
"message.new-version-available": "A new version of umami {version} is available!",
"message.no-data-available": "No data available.",
"message.no-websites-configured": "You don't have any websites configured.",
"message.page-not-found": "Page not found.",
"message.powered-by": "Powered by {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.save-success": "Saved successfully.",
"message.share-url": "This is the publicly shared URL for {target}.",
"message.toggle-charts": "Toggle charts",
"message.track-stats": "To track stats for {target}, place the following code in the {head} section of your website.",
"message.type-delete": "Type {delete} in the box below to confirm.",
"message.type-reset": "Type {reset} in the box below to confirm.",
"metrics.actions": "Actions",
"metrics.average-visit-time": "Average visit time",
"metrics.bounce-rate": "Bounce rate",
"metrics.browsers": "Browsers",
"metrics.countries": "Countries",
"metrics.device.desktop": "Desktop",
"metrics.device.laptop": "Laptop",
"metrics.device.mobile": "Mobile",
"metrics.device.tablet": "Tablet",
"metrics.devices": "Devices",
"metrics.events": "Events",
"metrics.filter.combined": "Combined",
"metrics.filter.raw": "Raw",
"metrics.languages": "Languages",
"metrics.operating-systems": "Operating systems",
"metrics.page-views": "Page views",
"metrics.pages": "Pages",
"metrics.query-parameters": "Query parameters",
"metrics.referrers": "Referrers",
"metrics.screens": "Screens",
"metrics.unique-visitors": "Unique visitors",
"metrics.views": "Views",
"metrics.visitors": "Visitors"
}

View File

@ -1,146 +0,0 @@
{
"label.access-code": "Access code",
"label.actions": "Aktione",
"label.activity-log": "Activity log",
"label.add-website": "Websiite hinzuefüege",
"label.admin": "Administrator",
"label.all": "Alli",
"label.all-time": "Gesamte Zitruum",
"label.analytics": "Analytics",
"label.average-visit-time": "Durchschn. Bsuechsziit",
"label.back": "Zrugg",
"label.bounce-rate": "Absprungsrate",
"label.browsers": "Browser",
"label.cancel": "Abbreche",
"label.change-password": "Passwort ändere",
"label.cities": "Cities",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.confirm-password": "Passwort widerhole",
"label.continue": "Continue",
"label.countries": "Länder",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.current-password": "Jetzigs Passwort",
"label.custom-range": "Benutzerdefinierte Bereich",
"label.dashboard": "Übersicht",
"label.data": "Data",
"label.date-range": "Datumsbereich",
"label.default-date-range": "Vorigstellte Datumsbereich",
"label.delete": "Lösche",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-website": "Websiite lösche",
"label.desktop": "Desktop",
"label.details": "Details",
"label.devices": "Grät",
"label.dismiss": "Verwerfe",
"label.domain": "Domain",
"label.edit": "Bearbeite",
"label.edit-dashboard": "Dashboard bearbeite",
"label.enable-share-url": "Freigab-URL aktiviere",
"label.events": "Ereigniss",
"label.filter-combined": "Kombiniert",
"label.filter-raw": "Rohdate",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Sprach",
"label.languages": "Sprache",
"label.laptop": "Laptop",
"label.last-days": "Letzti {x} Täg",
"label.last-hours": "Letzti {x} Stunde",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.login": "Login",
"label.logout": "Abmelde",
"label.members": "Members",
"label.mobile": "Handy",
"label.more": "Meh",
"label.name": "Name",
"label.new-password": "Neus Passwort",
"label.none": "Keis",
"label.operating-systems": "Betriebssystem",
"label.owner": "Bsitzer",
"label.page-views": "Siitenufrüef",
"label.pages": "Siite",
"label.password": "Passwort",
"label.powered-by": "Betribe dur {name}",
"label.profile": "Profil",
"label.queries": "Queries",
"label.query-parameters": "Abfragparameter",
"label.realtime": "Echtzit",
"label.referrers": "Referrer",
"label.refresh": "Aktualisiere",
"label.regenerate": "Regenerate",
"label.regions": "Regions",
"label.remove": "Remove",
"label.required": "Erforderlich",
"label.reset": "Zruggsetze",
"label.reset-website": "Statistik zruggsetze",
"label.role": "Role",
"label.save": "Speichere",
"label.screens": "Bildschirmuflösige",
"label.select-website": "Select website",
"label.sessions": "Sessions",
"label.settings": "Istellige",
"label.share-url": "Freigab-URL",
"label.single-day": "Ein Tag",
"label.tablet": "Tablet",
"label.team": "Team",
"label.team-guest": "Team guest",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-owner": "Team owner",
"label.teams": "Teams",
"label.theme": "Thema",
"label.this-month": "De Monet",
"label.this-week": "Die Wuche",
"label.this-year": "Das Jahr",
"label.timezone": "Zitzone",
"label.title": "Title",
"label.today": "Hüt",
"label.toggle-charts": "Schaubilder umschalte",
"label.tracking-code": "Tracking Code",
"label.unique-visitors": "Eidütigi Bsuecher",
"label.unknown": "Unbekannt",
"label.user": "User",
"label.username": "Benutzername",
"label.users": "Users",
"label.view": "View",
"label.view-details": "Details azeige",
"label.views": "Ufrüef",
"label.visitors": "Bsuecher",
"label.website-id": "Website ID",
"label.websites": "Websiite",
"label.yesterday": "Gester",
"message.active-users": "{x} {x, plural, one {aktive Bsuecher} other {aktivi Bsuecher}}",
"message.confirm-delete": "Sind Sie sich sicher, {target} zlösche?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-reset": "Sind Sie sicher, dass Sie dStatistike vo {target} zruggsetze wend?",
"message.delete-website": "Websiite lösche",
"message.delete-website-warning": "Alli dezueghörige Date werdet ebefalls glöscht.",
"message.error": "Es isch en Fehler uftrete.",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "Zu de Istellige",
"message.incorrect-username-password": "Falschs Passwort oder Benutzername.",
"message.invalid-domain": "Ungültigi Domain",
"message.min-password-length": "Minimum length of {n} characters",
"message.no-data-available": "Kei Date vorhande.",
"message.no-match-password": "Passwörter stimmed ned überi",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.page-not-found": "Siite ned gfunde.",
"message.reset-website": "Statistik zruggsetze",
"message.reset-website-warning": "Alli Date für die Websiite werdet glöscht, nur de Tracking Code blibt bestah.",
"message.saved": "Erfolgrich gspeichert.",
"message.share-url": "Das isch die öffentlichi URL zum Teile für {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.tracking-code": "Tracking Code",
"message.user-deleted": "User deleted.",
"message.visitor-log": "Bsuecher us {country} benutzt {browser} uf {os} {device}",
"messages.no-team-websites": "This team does not have any websites.",
"messages.no-websites-configured": "Es isch kei Websiite vorhande.",
"messages.team-websites-info": "Websites can be viewed by anyone on the team."
}

View File

@ -1,120 +0,0 @@
{
"label.accounts": "Računi",
"label.add-account": "Dodaj račun",
"label.add-column": "Dodaj stupac",
"label.add-filter": "Dodaj filter",
"label.add-website": "Dodaj web stranicu",
"label.administrator": "Administrator",
"label.all": "Sve",
"label.all-time": "Svo vrijeme",
"label.all-websites": "Sve web stranice",
"label.back": "Natrag ",
"label.cancel": "Odustani",
"label.change-password": "Promijeni lozinku",
"label.confirm-password": "Potvrdi lozinku",
"label.copy-to-clipboard": "Kopiraj u međuspremnik",
"label.current-password": "Trenutna lozinka",
"label.custom-range": "Prilagođeni raspon",
"label.dashboard": "Nadzorna ploča",
"label.date-range": "Raspon datuma",
"label.default-date-range": "Zadani datumski raspon",
"label.delete": "Obriši",
"label.delete-account": "Obriši račun",
"label.delete-website": "Obriši web stranicu",
"label.dismiss": "Odbaci",
"label.domain": "Domena",
"label.edit": "Uredi",
"label.edit-account": "Uredi račun",
"label.edit-website": "Uredi web stranicu",
"label.enable-share-url": "Omogući dijeljenje poveznice",
"label.event-data": "Podaci događaja",
"label.field-name": "Naziv polja",
"label.invalid": "Neispravno",
"label.invalid-domain": "Neispravna domena",
"label.language": "Jezik",
"label.last-days": "Zadnjih {x} dana",
"label.last-hours": "Zadnjih {x} sati",
"label.logged-in-as": "Prijavljen kao {username}",
"label.login": "Prijava",
"label.logout": "Odjava",
"label.more": "Više",
"label.name": "Ime",
"label.new-password": "Nova lozinka",
"label.none": "Ništa",
"label.owner": "Vlasnik",
"label.password": "Lozinka",
"label.passwords-dont-match": "Lozinke se ne podudaraju",
"label.profile": "Profil",
"label.realtime": "Stvarno vrijeme",
"label.realtime-logs": "Trenutni zapisi",
"label.refresh": "Osvježi",
"label.required": "Potrebna",
"label.reset": "Resetirati",
"label.reset-website": "Resetirati web stranicu",
"label.save": "Spremi",
"label.search": "Pretraži",
"label.settings": "Postavke",
"label.share-url": "Podijeli poveznicu",
"label.single-day": "Jedan dan",
"label.theme": "Tema",
"label.this-month": "Ovaj mjesec",
"label.this-week": "Ovaj tjedan",
"label.this-year": "Ova godina",
"label.timezone": "Vremenska zona",
"label.today": "Danas",
"label.tracking-code": "Kod za praćenje",
"label.type": "Tip",
"label.unknown": "Nepoznato",
"label.username": "Korisničko ime",
"label.value": "Vrijednost",
"label.view-details": "Pogledaj detalje",
"label.websites": "Web stranice",
"label.yesterday": "Jučer",
"message.active-users": "{x} Trenutno {x, plural, one {posjetitelj} other {posjetitelja}}",
"message.confirm-delete": "Jeste li sigurni da želite obrisati {target}?",
"message.confirm-reset": "Jeste li sigurni da želite resetirati {target}'s statistiku?",
"message.copied": "Kopirano!",
"message.delete-warning": "Izbrisat će se svi povezani podaci.",
"message.edit-dashboard": "Uredi nadzornu ploču",
"message.failure": "Nešto je pošlo po zlu.",
"message.get-share-url": "Dohvati poveznicu za dijeljenje",
"message.get-tracking-code": "Dohvati kod za praćenje",
"message.go-to-settings": "Idi u postavke",
"message.incorrect-username-password": "Neispravno korisničke ime/lozinka.",
"message.log.visitor": "Posjetitelj iz {country} koristi {browser} na {os} {device}",
"message.new-version-available": "Nova verzija umami {version} je dostupna!",
"message.no-data-available": "Nema dostupnih podataka.",
"message.no-websites-configured": "Nemate konfiguriranu nijednu web stranicu.",
"message.page-not-found": "Stranica nije pronađena.",
"message.powered-by": "Pokreće {name}",
"message.reset-warning": "Sve statistike za ovu web stranicu bit će izbrisane, ali će vaš kod za praćenje ostati netaknut.",
"message.save-success": "Uspješno spremljeno.",
"message.share-url": "Ovo je javno dijeljena poveznica za {target}.",
"message.toggle-charts": "Uključi/isključi grafikone",
"message.track-stats": "Da biste pratili statistiku za {target}, postavite sljedeći kod u odjeljak {head} svoje web stranice.",
"message.type-delete": "Upišite {delete} u donji okvir za potvrdu.",
"message.type-reset": " Upišite {reset} u donji okvir za potvrdu. ",
"metrics.actions": "Akcije",
"metrics.average-visit-time": "Prosječno vrijeme posjeta",
"metrics.bounce-rate": "Stopa napuštanja stranice",
"metrics.browsers": "Web preglednici",
"metrics.countries": "Zemlje",
"metrics.device.desktop": "Pc",
"metrics.device.laptop": "Laptop",
"metrics.device.mobile": "Mobitel",
"metrics.device.tablet": "Tablet",
"metrics.devices": "Uređaji",
"metrics.events": "Događaji",
"metrics.filter.combined": "Kombinirano",
"metrics.filter.raw": "Neobrađeni podaci",
"metrics.languages": "Jezici",
"metrics.operating-systems": "Operativni sustavi",
"metrics.page-views": "Pregledi stranice",
"metrics.pages": "Stranice",
"metrics.query-parameters": "Parametri upita",
"metrics.referrers": "Upučivaći",
"metrics.screens": "Zasloni",
"metrics.unique-visitors": "Jedinstveni posjetitelji",
"metrics.views": "Pregledi",
"metrics.visitors": "Posjetitelji"
}

View File

@ -1,146 +0,0 @@
{
"label.access-code": "Access code",
"label.actions": "アクション",
"label.activity-log": "Activity log",
"label.add-website": "Webサイトの追加",
"label.admin": "管理者",
"label.all": "すべて表示",
"label.all-time": "All time",
"label.analytics": "Analytics",
"label.average-visit-time": "平均滞在時間",
"label.back": "戻る",
"label.bounce-rate": "直帰率",
"label.browsers": "ブラウザ",
"label.cancel": "キャンセル",
"label.change-password": "パスワード変更",
"label.cities": "Cities",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.confirm-password": "パスワード(確認)",
"label.continue": "Continue",
"label.countries": "国",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.current-password": "現在のパスワード",
"label.custom-range": "期間を指定する",
"label.dashboard": "ダッシュボード",
"label.data": "Data",
"label.date-range": "範囲指定",
"label.default-date-range": "最初に表示する期間",
"label.delete": "削除",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-website": "Webサイトの削除",
"label.desktop": "デスクトップ",
"label.details": "Details",
"label.devices": "デバイス",
"label.dismiss": "無視する",
"label.domain": "ドメイン",
"label.edit": "編集",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "共有リンクを有効にする",
"label.events": "イベント",
"label.filter-combined": "パスまで",
"label.filter-raw": "すべて表示",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Language",
"label.languages": "Languages",
"label.laptop": "ートPC",
"label.last-days": "過去{x}日間",
"label.last-hours": "過去{x}時間",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.login": "ログイン",
"label.logout": "ログアウト",
"label.members": "Members",
"label.mobile": "携帯電話",
"label.more": "さらに表示",
"label.name": "名前",
"label.new-password": "新しいパスワード",
"label.none": "None",
"label.operating-systems": "OS",
"label.owner": "Owner",
"label.page-views": "閲覧数",
"label.pages": "ページ",
"label.password": "パスワード",
"label.powered-by": "このシステムは {name} で実行されています。",
"label.profile": "プロファイル",
"label.queries": "Queries",
"label.query-parameters": "Query parameters",
"label.realtime": "リアルタイム",
"label.referrers": "リファラー",
"label.refresh": "更新",
"label.regenerate": "Regenerate",
"label.regions": "Regions",
"label.remove": "Remove",
"label.required": "必須",
"label.reset": "リセット",
"label.reset-website": "Reset statistics",
"label.role": "Role",
"label.save": "保存",
"label.screens": "Screens",
"label.select-website": "Select website",
"label.sessions": "Sessions",
"label.settings": "設定",
"label.share-url": "共有リンク",
"label.single-day": "一日のみ",
"label.tablet": "タブレット",
"label.team": "Team",
"label.team-guest": "Team guest",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-owner": "Team owner",
"label.teams": "Teams",
"label.theme": "Theme",
"label.this-month": "今月",
"label.this-week": "今週",
"label.this-year": "今年",
"label.timezone": "タイムゾーン",
"label.title": "Title",
"label.today": "今日",
"label.toggle-charts": "Toggle charts",
"label.tracking-code": "トラッキングコード",
"label.unique-visitors": "ユニーク訪問者数",
"label.unknown": "不明",
"label.user": "User",
"label.username": "ユーザー名",
"label.users": "Users",
"label.view": "View",
"label.view-details": "詳細を見る",
"label.views": "閲覧数",
"label.visitors": "訪問者数",
"label.website-id": "Website ID",
"label.websites": "Webサイト",
"label.yesterday": "Yesterday",
"message.active-users": "{x}人が閲覧中です。",
"message.confirm-delete": "{target}を削除してもよろしいですか?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.delete-website": "Webサイトの削除",
"message.delete-website-warning": "関連するすべてのデータも削除されます。",
"message.error": "問題が発生しました。",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "設定する",
"message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。",
"message.invalid-domain": "無効なドメイン",
"message.min-password-length": "Minimum length of {n} characters",
"message.no-data-available": "データがありません。",
"message.no-match-password": "パスワードが一致しません",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.page-not-found": "ページが見つかりません。",
"message.reset-website": "Reset statistics",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "正常に保存されました。",
"message.share-url": "これは{target}の共有リンクです。",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.tracking-code": "トラッキングコード",
"message.user-deleted": "User deleted.",
"message.visitor-log": "{os}{device})で{browser}を使用している{country}からの訪問者",
"messages.no-team-websites": "This team does not have any websites.",
"messages.no-websites-configured": "Webサイトが設定されていません。",
"messages.team-websites-info": "Websites can be viewed by anyone on the team."
}

View File

@ -1,146 +0,0 @@
{
"label.access-code": "Access code",
"label.actions": "Үйлдлүүд",
"label.activity-log": "Activity log",
"label.add-website": "Веб нэмэх",
"label.admin": "Админ",
"label.all": "Бүх",
"label.all-time": "Бүх цаг үеийн",
"label.analytics": "Analytics",
"label.average-visit-time": "Зочилсон дундаж хугацаа",
"label.back": "Буцах",
"label.bounce-rate": "Нэг хуудас үзээд гарсан",
"label.browsers": "Хөтөч",
"label.cancel": "Цуцлах",
"label.change-password": "Нууц үг солих",
"label.cities": "Cities",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.confirm-password": "Шинэ нууц үгээ давтах",
"label.continue": "Continue",
"label.countries": "Улс",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.current-password": "Ашиглаж буй нууц үг",
"label.custom-range": "Дурын хугацаа",
"label.dashboard": "Хянах самбар",
"label.data": "Data",
"label.date-range": "Хугацааны мужид",
"label.default-date-range": "Өгөгдмөл хугацааны муж",
"label.delete": "Устгах",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-website": "Веб устгах",
"label.desktop": "Суурин компьютер",
"label.details": "Details",
"label.devices": "Төхөөрөмж",
"label.dismiss": "Үл хэргэсэх",
"label.domain": "Домэйн",
"label.edit": "Засах",
"label.edit-dashboard": "Хянах самбар засах",
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
"label.events": "Үйлдэл",
"label.filter-combined": "Нэгтгэсэн",
"label.filter-raw": "Түүхий",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Хэл",
"label.languages": "Хэл",
"label.laptop": "Зөөврийн компьютер",
"label.last-days": "Сүүлийн {x} хоног",
"label.last-hours": "Сүүлийн {x} цаг",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.login": "Нэвтрэх",
"label.logout": "Гарах",
"label.members": "Members",
"label.mobile": "Утас",
"label.more": "Цааш",
"label.name": "Нэр",
"label.new-password": "Шинэ нууц үг",
"label.none": "Байхгүй",
"label.operating-systems": "Үйлдлийн систем",
"label.owner": "Эзэмшигч",
"label.page-views": "Хуудас үзсэн",
"label.pages": "Хуудас",
"label.password": "Нууц үг",
"label.powered-by": "{name} дээр суурилсан",
"label.profile": "Бүртгэл",
"label.queries": "Queries",
"label.query-parameters": "Query параметр",
"label.realtime": "Яг одоо",
"label.referrers": "Чиглүүлэгч",
"label.refresh": "Сэргээх",
"label.regenerate": "Regenerate",
"label.regions": "Regions",
"label.remove": "Remove",
"label.required": "Шаардлагатай",
"label.reset": "Хуучин хэвд нь оруулах",
"label.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх",
"label.role": "Role",
"label.save": "Хадгалах",
"label.screens": "Дэлгэц",
"label.select-website": "Select website",
"label.sessions": "Sessions",
"label.settings": "Тохиргоо",
"label.share-url": "Хуваалцах холбоос",
"label.single-day": "Нэг өдөр",
"label.tablet": "Таблет",
"label.team": "Team",
"label.team-guest": "Team guest",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-owner": "Team owner",
"label.teams": "Teams",
"label.theme": "Загвар",
"label.this-month": "Энэ сар",
"label.this-week": "Энэ долоо хоног",
"label.this-year": "Энэ жил",
"label.timezone": "Цагийн бүс",
"label.title": "Title",
"label.today": "Өнөөдөр",
"label.toggle-charts": "Графикийг харуулах/нуух",
"label.tracking-code": "Мөрдөх код",
"label.unique-visitors": "Зочин",
"label.unknown": "Тодорхойгүй",
"label.user": "User",
"label.username": "Хэрэглэгчийн нэр",
"label.users": "Users",
"label.view": "View",
"label.view-details": "Дэлгэрүүлж харах",
"label.views": "Үзсэн",
"label.visitors": "Зочин",
"label.website-id": "Website ID",
"label.websites": "Вебүүд",
"label.yesterday": "Өчигдөр",
"message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна",
"message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-reset": "Та {target}-н тоон үзүүлэлтүүдийг устгахдаа итгэлтэй байна уу?",
"message.delete-website": "Веб устгах",
"message.delete-website-warning": "Үүнтэй холбоотой бүх өгөгдөл устах болно.",
"message.error": "Ямар нэг зүйл буруу боллоо.",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "Тохиргоо руу очих",
"message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.",
"message.invalid-domain": "Буруу домэйн",
"message.min-password-length": "Minimum length of {n} characters",
"message.no-data-available": "Өгөгдөл алга.",
"message.no-match-password": "Нууц үг тохирохгүй байна",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.page-not-found": "Хуудас олдсонгүй.",
"message.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх",
"message.reset-website-warning": "Энэ вебийн бүх тоон үзүүлэлтүүдийг устгах болно. Гэхдээ мөрдөх код хэвэндээ үлдэнэ.",
"message.saved": "Амжилттай хадгаллаа.",
"message.share-url": "{target}-г нийтэд хуваалцах холбоос.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.tracking-code": "Мөрдөх код",
"message.user-deleted": "User deleted.",
"message.visitor-log": "{country} улсаас {os} {device} дээр {browser} хөтөч ашиглан орсон",
"messages.no-team-websites": "This team does not have any websites.",
"messages.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.",
"messages.team-websites-info": "Websites can be viewed by anyone on the team."
}

View File

@ -1,146 +0,0 @@
{
"label.access-code": "Access code",
"label.actions": "Acties",
"label.activity-log": "Activity log",
"label.add-website": "Website toevoegen",
"label.admin": "Administrator",
"label.all": "Alles",
"label.all-time": "Onbeperkt",
"label.analytics": "Analytics",
"label.average-visit-time": "Gemiddelde bezoektijd",
"label.back": "Terug",
"label.bounce-rate": "Bouncepercentage",
"label.browsers": "Browsers",
"label.cancel": "Annuleren",
"label.change-password": "Wachtwoord wijzigen",
"label.cities": "Cities",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.confirm-password": "Wachtwoord bevestigen",
"label.continue": "Continue",
"label.countries": "Landen",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.current-password": "Huidig wachtwoord",
"label.custom-range": "Aangepast bereik",
"label.dashboard": "Overzicht",
"label.data": "Data",
"label.date-range": "Datumbereik",
"label.default-date-range": "Standaard bereik",
"label.delete": "Verwijderen",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-website": "Website verwijderen",
"label.desktop": "Desktop",
"label.details": "Details",
"label.devices": "Apparaten",
"label.dismiss": "Negeren",
"label.domain": "Domein",
"label.edit": "Bewerken",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Sta delen via openbare URL toe",
"label.events": "Gebeurtenissen",
"label.filter-combined": "Gecombineerd",
"label.filter-raw": "Ruw",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Taal",
"label.languages": "Languages",
"label.laptop": "Laptop",
"label.last-days": "Laatste {x} dagen",
"label.last-hours": "Laatste {x} uur",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.login": "Inloggen",
"label.logout": "Uitloggen",
"label.members": "Members",
"label.mobile": "Mobiel",
"label.more": "Toon meer",
"label.name": "Naam",
"label.new-password": "Nieuw wachtwoord",
"label.none": "Geen",
"label.operating-systems": "Besturingssystemen",
"label.owner": "Eigenaar",
"label.page-views": "Paginaweergaven",
"label.pages": "Pagina's",
"label.password": "Wachtwoord",
"label.powered-by": "mogelijk gemaakt door {name}",
"label.profile": "Profiel",
"label.queries": "Queries",
"label.query-parameters": "Query parameters",
"label.realtime": "Actueel",
"label.referrers": "Verwijzers",
"label.refresh": "Vernieuwen",
"label.regenerate": "Regenerate",
"label.regions": "Regions",
"label.remove": "Remove",
"label.required": "Verplicht",
"label.reset": "Resetten",
"label.reset-website": "Statistieken opnieuw instellen",
"label.role": "Role",
"label.save": "Opslaan",
"label.screens": "Schermen",
"label.select-website": "Select website",
"label.sessions": "Sessions",
"label.settings": "Instellingen",
"label.share-url": "URL delen",
"label.single-day": "Enkele dag",
"label.tablet": "Tablet",
"label.team": "Team",
"label.team-guest": "Team guest",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-owner": "Team owner",
"label.teams": "Teams",
"label.theme": "Thema",
"label.this-month": "Deze maand",
"label.this-week": "Deze week",
"label.this-year": "Dit jaar",
"label.timezone": "Tijdzone",
"label.title": "Title",
"label.today": "Vandaag",
"label.toggle-charts": "Grafieken tonen/verbergen",
"label.tracking-code": "Volgcode",
"label.unique-visitors": "Unieke bezoekers",
"label.unknown": "Onbekend",
"label.user": "User",
"label.username": "Gebruikersnaam",
"label.users": "Users",
"label.view": "View",
"label.view-details": "Meer details",
"label.views": "Weergaven",
"label.visitors": "Bezoekers",
"label.website-id": "Website ID",
"label.websites": "Websites",
"label.yesterday": "Yesterday",
"message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}",
"message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-reset": "Weet je zeker dat je de statistieken van {target} opnieuw wilt instellen?",
"message.delete-website": "Website verwijderen",
"message.delete-website-warning": "Alle verwante gegezens zullen ook verwijderd worden.",
"message.error": "Er is iets misgegaan.",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "Naar instellingen",
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
"message.invalid-domain": "Ongeldig domein",
"message.min-password-length": "Minimum length of {n} characters",
"message.no-data-available": "Geen gegevens beschikbaar.",
"message.no-match-password": "Wachtwoorden komen niet overeen",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.page-not-found": "Pagina niet gevonden.",
"message.reset-website": "Statistieken opnieuw instellen",
"message.reset-website-warning": "Alle bijhorende statistieken van deze website worden verwijderd, maar jouw volgcode blijft gelden.",
"message.saved": "Opslaan succesvol.",
"message.share-url": "Met deze URL kan {target} openbaar gedeeld worden.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.tracking-code": "Volgcode",
"message.user-deleted": "User deleted.",
"message.visitor-log": "Bezoeker uit {country} met {browser} op een {os} {device}",
"messages.no-team-websites": "This team does not have any websites.",
"messages.no-websites-configured": "Je hebt geen websites ingesteld.",
"messages.team-websites-info": "Websites can be viewed by anyone on the team."
}

View File

@ -1,146 +0,0 @@
{
"label.access-code": "Access code",
"label.actions": "Ações",
"label.activity-log": "Activity log",
"label.add-website": "Adicionar site",
"label.admin": "Administrador",
"label.all": "Todos",
"label.all-time": "Todo o período",
"label.analytics": "Analytics",
"label.average-visit-time": "Tempo médio da visita",
"label.back": "Voltar",
"label.bounce-rate": "Taxa de rejeição",
"label.browsers": "Navegadores",
"label.cancel": "Cancelar",
"label.change-password": "Alterar a senha",
"label.cities": "Cities",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.confirm-password": "Confirme a nova senha",
"label.continue": "Continue",
"label.countries": "Países",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.current-password": "Senha atual",
"label.custom-range": "Intervalo personalizado",
"label.dashboard": "Painel",
"label.data": "Data",
"label.date-range": "Intervalo de datas",
"label.default-date-range": "Intervalo de datas predefinido",
"label.delete": "Remover",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-website": "Remover site",
"label.desktop": "Computador",
"label.details": "Details",
"label.devices": "Dispositivos",
"label.dismiss": "Dispensar",
"label.domain": "Domínio",
"label.edit": "Editar",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Ativar link de compartilhamento",
"label.events": "Eventos",
"label.filter-combined": "Combinado",
"label.filter-raw": "Dados brutos",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Idioma",
"label.languages": "Idiomas",
"label.laptop": "Notebook",
"label.last-days": "Últimos {x} dias",
"label.last-hours": "Últimas {x} horas",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.login": "Iniciar sessão",
"label.logout": "Sair",
"label.members": "Members",
"label.mobile": "Celular",
"label.more": "Mais",
"label.name": "Nome",
"label.new-password": "Nova senha",
"label.none": "None",
"label.operating-systems": "Sistemas operacionais",
"label.owner": "Proprietário",
"label.page-views": "Visualizações de página",
"label.pages": "Páginas",
"label.password": "Senha",
"label.powered-by": "Distribuído por {name}",
"label.profile": "Perfil",
"label.queries": "Queries",
"label.query-parameters": "Parâmetros de Consulta",
"label.realtime": "Tempo real",
"label.referrers": "Referências",
"label.refresh": "Atualizar",
"label.regenerate": "Regenerate",
"label.regions": "Regions",
"label.remove": "Remove",
"label.required": "Obrigatório",
"label.reset": "Redefinir",
"label.reset-website": "Redefinir estatísticas",
"label.role": "Role",
"label.save": "Salvar",
"label.screens": "Telas",
"label.select-website": "Select website",
"label.sessions": "Sessions",
"label.settings": "Configurações",
"label.share-url": "Link de compartilhamento",
"label.single-day": "Dia específico",
"label.tablet": "Tablet",
"label.team": "Team",
"label.team-guest": "Team guest",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-owner": "Team owner",
"label.teams": "Teams",
"label.theme": "Tema",
"label.this-month": "Este mês",
"label.this-week": "Esta semana",
"label.this-year": "Este ano",
"label.timezone": "Fuso horário",
"label.title": "Title",
"label.today": "Hoje",
"label.toggle-charts": "Mostrar/Esconder gráficos",
"label.tracking-code": "Código de rastreamento",
"label.unique-visitors": "Visitantes únicos",
"label.unknown": "Desconhecido",
"label.user": "User",
"label.username": "Nome de usuário",
"label.users": "Users",
"label.view": "View",
"label.view-details": "Ver detalhes",
"label.views": "Visualizações",
"label.visitors": "Visitantes",
"label.website-id": "Website ID",
"label.websites": "Sites",
"label.yesterday": "Ontem",
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
"message.confirm-delete": "Deseja realmente remover {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-reset": "Você tem certeza que deseja redefinir as estatísticas de {target}?",
"message.delete-website": "Remover site",
"message.delete-website-warning": "Todos os dados associados também serão eliminados.",
"message.error": "Ocorreu um erro.",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "Ir para as configurações",
"message.incorrect-username-password": "O nome de usuário e/ou senha está incorreto.",
"message.invalid-domain": "Domínio inválido",
"message.min-password-length": "Minimum length of {n} characters",
"message.no-data-available": "Sem dados disponíveis.",
"message.no-match-password": "As senhas não correspondem",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.page-not-found": "Página não encontrada.",
"message.reset-website": "Redefinir estatísticas",
"message.reset-website-warning": "Todas as estatísticas deste site serão removidas, mas seu código de rastreamento permanecerá intacto.",
"message.saved": "Salvo com sucesso.",
"message.share-url": "Este é o link público de compartilhamento para {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.tracking-code": "Código de rastreamento",
"message.user-deleted": "User deleted.",
"message.visitor-log": "Visitante de {country} usando {browser} no {device} {os}",
"messages.no-team-websites": "This team does not have any websites.",
"messages.no-websites-configured": "Nenhum site foi configurado ainda.",
"messages.team-websites-info": "Websites can be viewed by anyone on the team."
}

View File

@ -1,120 +0,0 @@
{
"label.accounts": "ගිණුම්",
"label.add-account": "ගිණුම එකතු කරන්න",
"label.add-column": "තීරුව එක් කරන්න",
"label.add-filter": "පෙරහන එකතු කරන්න",
"label.add-website": "වෙබ් අඩවිය එක් කරන්න",
"label.administrator": "පරිපාලක",
"label.all": "සියල්ල",
"label.all-time": "හැම වෙලාවෙම",
"label.all-websites": "සියලුම වෙබ් අඩවි",
"label.back": "ආපසු",
"label.cancel": "අවලංගු කරන්න",
"label.change-password": "මුරපදය වෙනස් කරන්න",
"label.confirm-password": "මුරපදය සත්‍යාපනය කරන්න",
"label.copy-to-clipboard": "පසුරු පුවරුවට පිටපත් කරන්න",
"label.current-password": "වත්මන් මුරපදය",
"label.custom-range": "අභිරුචි පරාසය",
"label.dashboard": "උපකරණ පුවරුව",
"label.date-range": "දින පරාසය",
"label.default-date-range": "පෙරනිමි දින පරාසය",
"label.delete": "මකන්න",
"label.delete-account": "ගිණුම මකන්න",
"label.delete-website": "වෙබ් අඩවිය මකන්න",
"label.dismiss": "මගහරින්න",
"label.domain": "වසම",
"label.edit": "සංස්කරණය කරන්න",
"label.edit-account": "ගිණුම සංස්කරණය කරන්න",
"label.edit-website": "වෙබ් අඩවිය සංස්කරණය කරන්න",
"label.enable-share-url": "බෙදාගැනීමේ URL සබල කරන්න",
"label.event-data": "සිදුවීම් දත්ත",
"label.field-name": "ක්ෂේත්‍ර නාම",
"label.invalid": "වලංගු නැත",
"label.invalid-domain": "වලංගු නොවන වසමක්",
"label.language": "භාෂාව",
"label.last-days": "අන්තිම {x} දින",
"label.last-hours": "අන්තිම {x} පැය",
"label.logged-in-as": "ලොග් වී ඇත්තේ {username}",
"label.login": "ලොග් වෙන්න",
"label.logout": "පිටවීම",
"label.more": "තවත්",
"label.name": "නම",
"label.new-password": "අලුත් මුරපදය",
"label.none": "කිසිවක් නැත",
"label.owner": "හිමිකරු",
"label.password": "මුරපදය",
"label.passwords-dont-match": "මුරපද නොගැලපේ",
"label.profile": "පැතිකඩ",
"label.realtime": "තත්ය කාල",
"label.realtime-logs": "තත්‍ය කාලීන ලොග්",
"label.refresh": "නැවුම් කරන්න",
"label.required": "අවශ්‍යයි",
"label.reset": "යළි පිහිටුවන්න",
"label.reset-website": "සංඛ්යා ලේඛන නැවත සකසන්න",
"label.save": "සුරකින්න",
"label.search": "සෙවීම",
"label.settings": "සැකසුම්",
"label.share-url": "බෙදාගැනීමේ URL",
"label.single-day": "තනි දවස",
"label.theme": "තේමාව",
"label.this-month": "මෙ මාසය",
"label.this-week": "මේ සතිය",
"label.this-year": "මේ අවුරුද්ද",
"label.timezone": "වේලා කලාපය",
"label.today": "අද",
"label.tracking-code": "ලුහුබැඳීමේ කේතය",
"label.type": "වර්ගය",
"label.unknown": "නොදනී",
"label.username": "පරිශීලක නාමය",
"label.value": "වටිනාකම",
"label.view-details": "තොරතුරු පෙන්වන්න",
"label.websites": "වෙබ් අඩවි",
"label.yesterday": "ඊයේ",
"message.active-users": "{x} දැන් {x, plural, one {අමුත්තා} other {අමුත්තන්}}",
"message.confirm-delete": "{target} මකා දැමීම ගැන විශ්වාසද?",
"message.confirm-reset": "{target} ට අදාල සංඛ්‍යාලේඛන නැවත පිහිටුවීමට අවශ්‍යද?",
"message.copied": "පිටපත් කරගත්තා!",
"message.delete-warning": "සියලුම ආශ්‍රිත දත්ත ද මකා දැමෙනු ඇත.",
"message.edit-dashboard": "උපකරණ පුවරුව සංස්කරණය කරන්න",
"message.failure": "යම් ගැටලුවක් මතු වී ඇත.",
"message.get-share-url": "බෙදාගැනීමේ URL ලබා ගන්න",
"message.get-tracking-code": "ලුහුබැඳීමේ කේතය ලබා ගන්න",
"message.go-to-settings": "සැකසීම් වෙත යන්න",
"message.incorrect-username-password": "වැරදි පරිශීලක නාමය/මුරපදය.",
"message.log.visitor": "{country} වලින් පැමිණි අමුත්තකු {device} එකේ, මේ {os} එකේ, මේ {browser} එකෙන් ඉන්නවා",
"message.new-version-available": "umami අලුත්ම {version} වන අනුවාදය නිකුත් උනා!",
"message.no-data-available": "පෙන්වීමට දත්ත නොමැත.",
"message.no-websites-configured": "ඔබට වින්‍යාස කර ඇති වෙබ් අඩවි කිසිවක් නොමැත.",
"message.page-not-found": "පිටුව හමු නොවීය.",
"message.powered-by": "බල ගැන්වුයේ {name}",
"message.reset-warning": "සියලුම සංඛ්‍යාලේඛන මකා දමනු ඇත. නමුත් ඔබගේ නිරීක්ෂණ කේතය නොවෙනස්ව පවතිනු ඇත.",
"message.save-success": "සාර්තකව සුරැකිණි.",
"message.share-url": "මේ {target} සඳහා ප්‍රසිද්ධියේ බෙදාගත් URL එකයි.",
"message.toggle-charts": "ප්‍රස්ථාර ටොගල් කරන්න",
"message.track-stats": "{target} හි සංඛ්යාලේඛන බැලීම සදහා, පහත කේතය {head} කොටසට ඇතුලත් කරන්න.",
"message.type-delete": "සත්‍යාපනය සදහා {delete} ලෙස පහල කොටුවේ ටයිප් කරන්න",
"message.type-reset": "සත්‍යාපනය සදහා {reset} ලෙස පහල කොටුවේ ටයිප් කරන්න",
"metrics.actions": "ක්රියාවන්",
"metrics.average-visit-time": "සාමාන්‍ය සංචාර කාලය",
"metrics.bounce-rate": "හැරී යන ප්‍රමාණය",
"metrics.browsers": "බ්‍රව්සර්",
"metrics.countries": "රටවල්",
"metrics.device.desktop": "ඩෙස්ක්ටොප්",
"metrics.device.laptop": "ලැප්ටොප්",
"metrics.device.mobile": "ජංගම",
"metrics.device.tablet": "ටැබ්ලට්",
"metrics.devices": "උපකරණ",
"metrics.events": "සිද්ධීන්",
"metrics.filter.combined": "ඒකාබද්ධ",
"metrics.filter.raw": "අමු",
"metrics.languages": "භාෂා",
"metrics.operating-systems": "මෙහෙයුම් පද්ධති",
"metrics.page-views": "පිටු බැලීම්",
"metrics.pages": "පිටු",
"metrics.query-parameters": "විමසුම් පරාමිතීන්",
"metrics.referrers": "යොමු කරන්නන්",
"metrics.screens": "තිර",
"metrics.unique-visitors": "අලුත්ම අමුත්තන්",
"metrics.views": "බැලූ ගණන",
"metrics.visitors": "අමුත්තන්"
}

View File

@ -1,146 +0,0 @@
{
"label.access-code": "Access code",
"label.actions": "Händelser",
"label.activity-log": "Activity log",
"label.add-website": "Lägg till webbsajt",
"label.admin": "Administratör",
"label.all": "Alla",
"label.all-time": "Sedan början",
"label.analytics": "Analytics",
"label.average-visit-time": "Medelbesökstid",
"label.back": "Tillbaka",
"label.bounce-rate": "Avvisningfrekvens",
"label.browsers": "Webbläsare",
"label.cancel": "Avbryt",
"label.change-password": "Byt lösenord",
"label.cities": "Cities",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.confirm-password": "Bekräfta lösenord",
"label.continue": "Continue",
"label.countries": "Länder",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.current-password": "Nuvarande lösenord",
"label.custom-range": "Anpassat urval",
"label.dashboard": "Översikt",
"label.data": "Data",
"label.date-range": "Datumomfång",
"label.default-date-range": "Standard datum-urval",
"label.delete": "Radera",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-website": "Radera webbsajt",
"label.desktop": "Stationär",
"label.details": "Details",
"label.devices": "Enheter",
"label.dismiss": "Avbryt",
"label.domain": "Domän",
"label.edit": "Redigera",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Aktivera delnings-URL",
"label.events": "Händelser",
"label.filter-combined": "Kombinerade",
"label.filter-raw": "Rådata",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "Språk",
"label.languages": "Språk",
"label.laptop": "Bärbar",
"label.last-days": "Senaste {x} dagarna",
"label.last-hours": "Senaste {x} timmarna",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.login": "Logga in",
"label.logout": "Logga ut",
"label.members": "Members",
"label.mobile": "Mobil",
"label.more": "Mer",
"label.name": "Namn",
"label.new-password": "Nytt lösenord",
"label.none": "None",
"label.operating-systems": "Operativsystem",
"label.owner": "Ägare",
"label.page-views": "Sidvisningar",
"label.pages": "Sidor",
"label.password": "Lösenord",
"label.powered-by": "Drivs av {name}",
"label.profile": "Profil",
"label.queries": "Queries",
"label.query-parameters": "Query parameters",
"label.realtime": "Realtid",
"label.referrers": "Hänvisare",
"label.refresh": "Uppdatera",
"label.regenerate": "Regenerate",
"label.regions": "Regions",
"label.remove": "Remove",
"label.required": "Krävs",
"label.reset": "Återställ",
"label.reset-website": "Återställ statistik",
"label.role": "Role",
"label.save": "Spara",
"label.screens": "Screens",
"label.select-website": "Select website",
"label.sessions": "Sessions",
"label.settings": "Inställningar",
"label.share-url": "Delnings-URL",
"label.single-day": "En dag",
"label.tablet": "Platta",
"label.team": "Team",
"label.team-guest": "Team guest",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-owner": "Team owner",
"label.teams": "Teams",
"label.theme": "Tema",
"label.this-month": "Denna månad",
"label.this-week": "Denna vecka",
"label.this-year": "Detta år",
"label.timezone": "Tidszon",
"label.title": "Title",
"label.today": "Idag",
"label.toggle-charts": "Visa/göm grafer",
"label.tracking-code": "Spårningskod",
"label.unique-visitors": "Unika besökare",
"label.unknown": "Okänd",
"label.user": "User",
"label.username": "Användarnamn",
"label.users": "Users",
"label.view": "View",
"label.view-details": "Visa detaljer",
"label.views": "Visningar",
"label.visitors": "Besökare",
"label.website-id": "Website ID",
"label.websites": "Webbsajt",
"label.yesterday": "Yesterday",
"message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu",
"message.confirm-delete": "Är du säker på att du vill radera {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-reset": "Är du säker på att du vill återställa statistiken för {target}?",
"message.delete-website": "Radera webbsajt",
"message.delete-website-warning": "All tillhörande data kommer också raderas.",
"message.error": "Något gick fel.",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "Gå till inställningar",
"message.incorrect-username-password": "Felaktigt användarnamn/lösenord.",
"message.invalid-domain": "Ogiltig domän",
"message.min-password-length": "Minimum length of {n} characters",
"message.no-data-available": "Ingen data tillgänglig.",
"message.no-match-password": "Lösenorden är inte samma",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.page-not-found": "Sidan kan inte hittas.",
"message.reset-website": "Återställ statistik",
"message.reset-website-warning": "All statistik för webbsajten tas bort men spårningskoden förblir oförändrad.",
"message.saved": "Sparades!",
"message.share-url": "Det här är den offentliga delnings-URL:en för {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.tracking-code": "Spårningskod",
"message.user-deleted": "User deleted.",
"message.visitor-log": "Besökare från {country} med {browser} på {os} {device}",
"messages.no-team-websites": "This team does not have any websites.",
"messages.no-websites-configured": "Du har inga webbsajter.",
"messages.team-websites-info": "Websites can be viewed by anyone on the team."
}

View File

@ -1,146 +0,0 @@
{
"label.access-code": "Access code",
"label.actions": "用戶行為",
"label.activity-log": "Activity log",
"label.add-website": "增加網站",
"label.admin": "管理員",
"label.all": "所有",
"label.all-time": "所有時間段",
"label.analytics": "Analytics",
"label.average-visit-time": "平均訪問時間",
"label.back": "返回",
"label.bounce-rate": "跳出率",
"label.browsers": "瀏覽器",
"label.cancel": "取消",
"label.change-password": "更新密碼",
"label.cities": "Cities",
"label.clear-all": "Clear all",
"label.confirm": "Confirm",
"label.confirm-password": "確認密碼",
"label.continue": "Continue",
"label.countries": "國家/地區",
"label.create-team": "Create team",
"label.create-user": "Create user",
"label.created": "Created",
"label.current-password": "目前密碼",
"label.custom-range": "自定義時段",
"label.dashboard": "管理面板",
"label.data": "Data",
"label.date-range": "多日",
"label.default-date-range": "默認日期範圍",
"label.delete": "刪除",
"label.delete-team": "Delete team",
"label.delete-user": "Delete user",
"label.delete-website": "刪除網站",
"label.desktop": "桌機",
"label.details": "Details",
"label.devices": "裝置",
"label.dismiss": "關閉",
"label.domain": "域名",
"label.edit": "編輯",
"label.edit-dashboard": "編輯管理面板",
"label.enable-share-url": "啟用分享連結",
"label.events": "行為類別",
"label.filter-combined": "總和",
"label.filter-raw": "原始",
"label.join": "Join",
"label.join-team": "Join team",
"label.language": "語言",
"label.languages": "語言",
"label.laptop": "筆記本",
"label.last-days": "最近 {x} 天",
"label.last-hours": "最近 {x} 小時",
"label.leave": "Leave",
"label.leave-team": "Leave team",
"label.login": "登入",
"label.logout": "退出",
"label.members": "Members",
"label.mobile": "手機",
"label.more": "更多",
"label.name": "名字",
"label.new-password": "新密碼",
"label.none": "無",
"label.operating-systems": "操作系統",
"label.owner": "擁有者",
"label.page-views": "網頁流量",
"label.pages": "網頁",
"label.password": "密碼",
"label.powered-by": "運行 {name}",
"label.profile": "個人資料",
"label.queries": "Queries",
"label.query-parameters": "查詢參數",
"label.realtime": "實時",
"label.referrers": "指入域名",
"label.refresh": "刷新",
"label.regenerate": "Regenerate",
"label.regions": "Regions",
"label.remove": "Remove",
"label.required": "必填",
"label.reset": "重置",
"label.reset-website": "重置統計數據",
"label.role": "Role",
"label.save": "保存",
"label.screens": "屏幕尺寸",
"label.select-website": "Select website",
"label.sessions": "Sessions",
"label.settings": "設置",
"label.share-url": "分享連結",
"label.single-day": "單日",
"label.tablet": "平板",
"label.team": "Team",
"label.team-guest": "Team guest",
"label.team-id": "Team ID",
"label.team-member": "Team member",
"label.team-owner": "Team owner",
"label.teams": "Teams",
"label.theme": "主題",
"label.this-month": "本月",
"label.this-week": "本週",
"label.this-year": "今年",
"label.timezone": "時區",
"label.title": "Title",
"label.today": "今天",
"label.toggle-charts": "切換圖表",
"label.tracking-code": "追蹤代碼",
"label.unique-visitors": "獨立訪客",
"label.unknown": "未知",
"label.user": "User",
"label.username": "用户名",
"label.users": "Users",
"label.view": "View",
"label.view-details": "查看更多",
"label.views": "頁面流量",
"label.visitors": "獨立訪客",
"label.website-id": "Website ID",
"label.websites": "網站",
"label.yesterday": "Yesterday",
"message.active-users": "當前線上 {x} 人",
"message.confirm-delete": "你確定要刪除 {target} 嗎?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
"message.confirm-reset": "您確定要重置 {target} 的數據嗎?",
"message.delete-website": "刪除網站",
"message.delete-website-warning": "所有相關數據將會被刪除。",
"message.error": "出現錯誤。",
"message.event-log": "{event} on {url}",
"message.go-to-settings": "去設定",
"message.incorrect-username-password": "用户名或密碼不正確。",
"message.invalid-domain": "無效域名",
"message.min-password-length": "Minimum length of {n} characters",
"message.no-data-available": "無可用數據。",
"message.no-match-password": "密碼不一致",
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.page-not-found": "網頁未找到。",
"message.reset-website": "重置統計數據",
"message.reset-website-warning": "本網站的所有統計數據將被刪除,但您的跟蹤代碼將保持不變。",
"message.saved": "成功保存。",
"message.share-url": "這是 {target} 的分享連結。",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
"message.tracking-code": "追蹤代碼",
"message.user-deleted": "User deleted.",
"message.visitor-log": "來自{country}的訪客在搭載 {os} 的{device}上使用 {browser} 進行訪問。",
"messages.no-team-websites": "This team does not have any websites.",
"messages.no-websites-configured": "目前無任何網站設定。",
"messages.team-websites-info": "Websites can be viewed by anyone on the team."
}

View File

@ -1,162 +0,0 @@
import prisma from '@umami/prisma-client';
import moment from 'moment-timezone';
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
import { getEventDataType } from './eventData';
import { FILTER_COLUMNS } from './constants';
const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%d %H:%i:00',
hour: '%Y-%m-%d %H:00:00',
day: '%Y-%m-%d',
month: '%Y-%m-01',
year: '%Y-01-01',
};
const POSTGRESQL_DATE_FORMATS = {
minute: 'YYYY-MM-DD HH24:MI:00',
hour: 'YYYY-MM-DD HH24:00:00',
day: 'YYYY-MM-DD',
month: 'YYYY-MM-01',
year: 'YYYY-01-01',
};
function toUuid(): string {
const db = getDatabaseType(process.env.DATABASE_URL);
if (db === POSTGRESQL) {
return '::uuid';
}
if (db === MYSQL) {
return '';
}
}
function getDateQuery(field: string, unit: string, timezone?: string): string {
const db = getDatabaseType(process.env.DATABASE_URL);
if (db === POSTGRESQL) {
if (timezone) {
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
}
return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
}
if (db === MYSQL) {
if (timezone) {
const tz = moment.tz(timezone).format('Z');
return `date_format(convert_tz(${field},'+00:00','${tz}'), '${MYSQL_DATE_FORMATS[unit]}')`;
}
return `date_format(${field}, '${MYSQL_DATE_FORMATS[unit]}')`;
}
}
function getTimestampInterval(field: string): string {
const db = getDatabaseType(process.env.DATABASE_URL);
if (db === POSTGRESQL) {
return `floor(extract(epoch from max(${field}) - min(${field})))`;
}
if (db === MYSQL) {
return `floor(unix_timestamp(max(${field})) - unix_timestamp(min(${field})))`;
}
}
function getEventDataFilterQuery(
filters: {
eventKey?: string;
eventValue?: string | number | boolean | Date;
}[],
params: any[],
) {
const query = filters.reduce((ac, cv) => {
const type = getEventDataType(cv.eventValue);
let value = cv.eventValue;
ac.push(`and (event_key = $${params.length + 1}`);
params.push(cv.eventKey);
switch (type) {
case 'number':
ac.push(`and event_numeric_value = $${params.length + 1})`);
params.push(value);
break;
case 'string':
ac.push(`and event_string_value = $${params.length + 1})`);
params.push(decodeURIComponent(cv.eventValue as string));
break;
case 'boolean':
ac.push(`and event_string_value = $${params.length + 1})`);
params.push(decodeURIComponent(cv.eventValue as string));
value = cv ? 'true' : 'false';
break;
case 'date':
ac.push(`and event_date_value = $${params.length + 1})`);
params.push(cv.eventValue);
break;
}
return ac;
}, []);
return query.join('\n');
}
function getFilterQuery(filters = {}, params = []): string {
const query = Object.keys(filters).reduce((arr, key) => {
const filter = filters[key];
if (filter !== undefined) {
const column = FILTER_COLUMNS[key] || key;
arr.push(`and ${column}=$${params.length + 1}`);
params.push(decodeURIComponent(filter));
}
return arr;
}, []);
return query.join('\n');
}
function parseFilters(
filters: { [key: string]: any } = {},
params = [],
sessionKey = 'session_id',
) {
const { os, browser, device, country, region, city } = filters;
return {
joinSession:
os || browser || device || country || region || city
? `inner join session on website_event.${sessionKey} = session.${sessionKey}`
: '',
filterQuery: getFilterQuery(filters, params),
};
}
async function rawQuery(query: string, params: never[] = []): Promise<any> {
const db = getDatabaseType(process.env.DATABASE_URL);
if (db !== POSTGRESQL && db !== MYSQL) {
return Promise.reject(new Error('Unknown database.'));
}
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
return prisma.rawQuery(sql, params);
}
export default {
...prisma,
getDateQuery,
getTimestampInterval,
getFilterQuery,
getEventDataFilterQuery,
toUuid,
parseFilters,
rawQuery,
};

View File

@ -1,131 +0,0 @@
import { NextApiRequest } from 'next';
import { EVENT_DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants';
type ObjectValues<T> = T[keyof T];
export type Roles = ObjectValues<typeof ROLES>;
export type EventTypes = ObjectValues<typeof EVENT_TYPE>;
export type EventDataTypes = ObjectValues<typeof EVENT_DATA_TYPE>;
export type KafkaTopics = ObjectValues<typeof KAFKA_TOPIC>;
export interface EventData {
[key: string]: number | string | EventData | number[] | string[] | EventData[];
}
export interface Auth {
user?: {
id: string;
username: string;
role: string;
isAdmin: boolean;
};
shareToken?: {
websiteId: string;
};
}
export interface NextApiRequestQueryBody<TQuery = any, TBody = any> extends NextApiRequest {
auth?: Auth;
query: TQuery & { [key: string]: string | string[] };
body: TBody;
headers: any;
}
export interface NextApiRequestAuth extends NextApiRequest {
auth?: Auth;
headers: any;
}
export interface User {
id: string;
username: string;
password?: string;
role: string;
createdAt?: Date;
}
export interface Website {
id: string;
userId: string;
resetAt: Date;
name: string;
domain: string;
shareId: string;
createdAt: Date;
}
export interface Share {
id: string;
token: string;
}
export interface WebsiteActive {
x: number;
}
export interface WebsiteMetric {
x: string;
y: number;
}
export interface WebsiteMetricFilter {
domain?: string;
url?: string;
referrer?: string;
title?: string;
query?: string;
event?: string;
os?: string;
browser?: string;
device?: string;
country?: string;
region?: string;
city?: string;
}
export interface WebsiteEventMetric {
x: string;
t: string;
y: number;
}
export interface WebsiteEventDataMetric {
x: string;
t: string;
eventName?: string;
urlPath?: string;
}
export interface WebsitePageviews {
pageviews: {
t: string;
y: number;
};
sessions: {
t: string;
y: number;
};
}
export interface WebsiteStats {
pageviews: { value: number; change: number };
uniques: { value: number; change: number };
bounces: { value: number; change: number };
totalTime: { value: number; change: number };
}
export interface RealtimeInit {
websites: Website[];
token: string;
data: RealtimeUpdate;
}
export interface RealtimeUpdate {
pageviews: any[];
sessions: any[];
events: any[];
timestamp: number;
}

View File

@ -1,16 +1,15 @@
/* eslint-disable @typescript-eslint/no-var-requires */
require('dotenv').config();
const path = require('path');
const pkg = require('./package.json');
const CLOUD_URL = 'https://cloud.umami.is';
const contentSecurityPolicy = `
default-src 'self';
img-src *;
script-src 'self' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
connect-src 'self' api.umami.is;
frame-ancestors 'self';
frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS};
`;
const headers = [
@ -60,12 +59,14 @@ if (process.env.TRACKER_SCRIPT_NAME) {
const redirects = [
{
source: '/settings',
destination: process.env.CLOUD_MODE ? '/settings/profile' : '/settings/websites',
destination: process.env.CLOUD_MODE
? `${process.env.CLOUD_URL}/settings/websites`
: '/settings/websites',
permanent: true,
},
];
if (process.env.CLOUD_MODE && process.env.DISABLE_LOGIN && process.env.CLOUD_URL) {
if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN) {
redirects.push({
source: '/login',
destination: process.env.CLOUD_URL,
@ -75,7 +76,11 @@ if (process.env.CLOUD_MODE && process.env.DISABLE_LOGIN && process.env.CLOUD_URL
const config = {
env: {
cloudMode: process.env.CLOUD_MODE,
cloudUrl: process.env.CLOUD_URL,
configUrl: '/config',
currentVersion: pkg.version,
defaultLocale: process.env.DEFAULT_LOCALE,
isProduction: process.env.NODE_ENV === 'production',
},
basePath: process.env.BASE_PATH,
@ -93,6 +98,8 @@ const config = {
use: ['@svgr/webpack'],
});
config.resolve.alias['public'] = path.resolve('./public');
return config;
},
async headers() {

23
package.components.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "@umami/components",
"version": "0.11.0",
"description": "Umami React components.",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",
"peerDependencies": {
"@tanstack/react-query": "^4.33.0",
"classnames": "^2.3.1",
"colord": "^2.9.2",
"immer": "^9.0.12",
"moment-timezone": "^0.5.35",
"next": "^13.4.0",
"next-basics": "^0.36.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intl": "^5.24.7",
"zustand": "^4.3.8"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "umami",
"version": "2.2.0",
"version": "2.6.0",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
@ -11,28 +11,31 @@
},
"scripts": {
"dev": "next dev -p 3000",
"build": "npm-run-all build-db check-db build-tracker build-geo build-app",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
"start-docker": "npm-run-all check-db update-tracker start-server",
"start-env": "node scripts/start-env.js",
"start-server": "node server.js",
"build-app": "next build",
"build-tracker": "rollup -c rollup.tracker.config.js",
"build-components": "rollup -c rollup.components.config.mjs",
"build-tracker": "rollup -c rollup.tracker.config.mjs",
"build-db": "npm-run-all copy-db-files build-db-client",
"build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names",
"build-lang": "npm-run-all format-lang compile-lang clean-lang download-country-names download-language-names",
"build-geo": "node scripts/build-geo.js",
"build-db-schema": "prisma db pull",
"build-db-client": "prisma generate",
"update-tracker": "node scripts/update-tracker.js",
"update-db": "prisma migrate deploy",
"check-db": "node scripts/check-db.js",
"check-env": "node scripts/check-env.js",
"copy-db-files": "node scripts/copy-db-files.js",
"extract-messages": "formatjs extract \"{pages,components}/**/*.js\" --out-file build/messages.json",
"merge-messages": "node scripts/merge-messages.js",
"generate-lang": "npm-run-all extract-messages merge-messages",
"format-lang": "node scripts/format-lang.js",
"compile-lang": "formatjs compile-folder --ast build/messages public/intl/messages",
"clean-lang": "prettier --write ./public/intl/messages/*.json",
"check-lang": "node scripts/check-lang.js",
"download-country-names": "node scripts/download-country-names.js",
"download-language-names": "node scripts/download-language-names.js",
@ -59,10 +62,10 @@
],
"dependencies": {
"@fontsource/inter": "^4.5.15",
"@prisma/client": "4.13.0",
"@tanstack/react-query": "^4.16.1",
"@prisma/client": "5.2.0",
"@tanstack/react-query": "^4.33.0",
"@umami/prisma-client": "^0.2.0",
"@umami/redis-client": "^0.2.0",
"@umami/redis-client": "^0.5.0",
"chalk": "^4.1.1",
"chart.js": "^4.2.1",
"chartjs-adapter-date-fns": "^3.0.0",
@ -78,7 +81,6 @@
"del": "^6.0.0",
"detect-browser": "^5.2.0",
"dotenv": "^10.0.0",
"formik": "^2.2.9",
"fs-extra": "^10.0.1",
"immer": "^9.0.12",
"ipaddr.js": "^2.0.1",
@ -89,12 +91,12 @@
"kafkajs": "^2.1.0",
"maxmind": "^4.3.6",
"moment-timezone": "^0.5.35",
"next": "13.2.4",
"next-basics": "^0.27.0",
"next": "13.4.19",
"next-basics": "^0.36.0",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"react": "^18.2.0",
"react-basics": "^0.77.0",
"react-basics": "^0.98.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4",
@ -104,24 +106,27 @@
"react-use-measure": "^2.0.4",
"react-window": "^1.8.6",
"request-ip": "^3.3.0",
"semver": "^7.3.6",
"semver": "^7.5.2",
"thenby": "^1.3.4",
"timezone-support": "^2.0.2",
"uuid": "^8.3.2",
"uuid": "^9.0.0",
"yup": "^0.32.11",
"zustand": "^3.7.2"
"zustand": "^4.3.8"
},
"devDependencies": {
"@formatjs/cli": "^4.2.29",
"@netlify/plugin-nextjs": "^4.27.3",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-buble": "^1.0.2",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^4.0.0",
"@svgr/rollup": "^7.0.0",
"@rollup/plugin-node-resolve": "^15.2.0",
"@rollup/plugin-replace": "^5.0.2",
"@svgr/rollup": "^8.1.0",
"@svgr/webpack": "^6.2.1",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.8",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"cross-env": "^7.0.3",
@ -141,21 +146,22 @@
"postcss-preset-env": "7.8.3",
"postcss-rtlcss": "^4.0.1",
"prettier": "^2.6.2",
"prisma": "4.13.0",
"prisma": "5.2.0",
"prompts": "2.4.2",
"rollup": "^2.70.1",
"rollup": "^3.28.0",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-dts": "^5.3.1",
"rollup-plugin-esbuild": "^5.0.0",
"rollup-plugin-node-externals": "^5.1.2",
"rollup-plugin-node-externals": "^6.1.1",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"stylelint": "^14.16.1",
"stylelint": "^15.10.1",
"stylelint-config-css-modules": "^4.1.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^9.0.0",
"tar": "^6.1.2",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.1.6"
}
}

View File

@ -1,62 +0,0 @@
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Head from 'next/head';
import Script from 'next/script';
import { useRouter } from 'next/router';
import ErrorBoundary from 'components/common/ErrorBoundary';
import useLocale from 'hooks/useLocale';
import useConfig from 'hooks/useConfig';
import '@fontsource/inter/400.css';
import '@fontsource/inter/700.css';
import 'react-basics/dist/styles.css';
import 'styles/variables.css';
import 'styles/locale.css';
import 'styles/index.css';
import 'chartjs-adapter-date-fns';
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
export default function App({ Component, pageProps }) {
const { locale, messages } = useLocale();
const { basePath, pathname } = useRouter();
const config = useConfig();
const Wrapper = ({ children }) => <span className={locale}>{children}</span>;
if (config?.uiDisabled) {
return null;
}
return (
<QueryClientProvider client={client}>
<IntlProvider
locale={locale}
messages={messages[locale]}
textComponent={Wrapper}
onError={() => null}
>
<ErrorBoundary>
<Head>
<link rel="icon" href={`${basePath}/favicon.ico`} sizes="any" />
<link rel="icon" href={`${basePath}/favicon.svg`} type="image/svg+xml" />
<link rel="apple-touch-icon" href={`${basePath}/apple-touch-icon.png`} />
<link rel="manifest" href={`${basePath}/site.webmanifest`} />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#e7eef4" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#1d2224" media="(prefers-color-scheme: dark)" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<Component {...pageProps} />
{!pathname.includes('/share/') && <Script src={`${basePath}/telemetry.js`} />}
</ErrorBoundary>
</IntlProvider>
</QueryClientProvider>
);
}

View File

@ -1,31 +0,0 @@
import { useAuth, useCors } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok } from 'next-basics';
import { getUserWebsites } from 'queries';
export interface WebsitesRequestBody {
name: string;
domain: string;
shareId: string;
}
export default async (
req: NextApiRequestQueryBody<any, WebsitesRequestBody>,
res: NextApiResponse,
) => {
await useCors(req, res);
await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
if (req.method === 'GET') {
const websites = await getUserWebsites(userId);
return ok(res, websites);
}
return methodNotAllowed(res);
};

View File

@ -1,25 +0,0 @@
import { subMinutes } from 'date-fns';
import { RealtimeInit, NextApiRequestAuth } from 'lib/types';
import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok } from 'next-basics';
import { getRealtimeData } from 'queries';
export default async (req: NextApiRequestAuth, res: NextApiResponse<RealtimeInit>) => {
await useAuth(req, res);
if (req.method === 'GET') {
const { id, startAt } = req.query;
let startTime = subMinutes(new Date(), 30);
if (+startAt > startTime.getTime()) {
startTime = new Date(+startAt);
}
const data = await getRealtimeData(id, startTime);
return ok(res, data);
}
return methodNotAllowed(res);
};

View File

@ -1,55 +0,0 @@
import { canUpdateTeam, canViewTeam } from 'lib/auth';
import { useAuth } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeamUser, getTeamUsers, getUser } from 'queries';
export interface TeamUserRequestQuery {
id: string;
}
export interface TeamUserRequestBody {
email: string;
roleId: string;
}
export default async (
req: NextApiRequestQueryBody<TeamUserRequestQuery, TeamUserRequestBody>,
res: NextApiResponse,
) => {
await useAuth(req, res);
const { id: teamId } = req.query;
if (req.method === 'GET') {
if (!(await canViewTeam(req.auth, teamId))) {
return unauthorized(res);
}
const users = await getTeamUsers(teamId);
return ok(res, users);
}
if (req.method === 'POST') {
if (!(await canUpdateTeam(req.auth, teamId))) {
return unauthorized(res, 'You must be the owner of this team.');
}
const { email, roleId: roleId } = req.body;
// Check for User
const user = await getUser({ username: email });
if (!user) {
return badRequest(res, 'The User does not exists.');
}
const updated = await createTeamUser(user.id, teamId, roleId);
return ok(res, updated);
}
return methodNotAllowed(res);
};

View File

@ -1,47 +0,0 @@
import { canViewTeam } from 'lib/auth';
import { useAuth } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeamWebsites, getTeamWebsites } from 'queries/admin/teamWebsite';
export interface TeamWebsiteRequestQuery {
id: string;
}
export interface TeamWebsiteRequestBody {
websiteIds?: string[];
}
export default async (
req: NextApiRequestQueryBody<TeamWebsiteRequestQuery, TeamWebsiteRequestBody>,
res: NextApiResponse,
) => {
await useAuth(req, res);
const { id: teamId } = req.query;
if (req.method === 'GET') {
if (!(await canViewTeam(req.auth, teamId))) {
return unauthorized(res);
}
const websites = await getTeamWebsites(teamId);
return ok(res, websites);
}
if (req.method === 'POST') {
if (!(await canViewTeam(req.auth, teamId))) {
return unauthorized(res);
}
const { websiteIds } = req.body;
const websites = await createTeamWebsites(teamId, websiteIds);
return ok(res, websites);
}
return methodNotAllowed(res);
};

View File

@ -1,50 +0,0 @@
import { Team } from '@prisma/client';
import { NextApiRequestQueryBody } from 'lib/types';
import { canCreateTeam } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next';
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeam, getUserTeams } from 'queries';
export interface TeamsRequestBody {
name: string;
}
export default async (
req: NextApiRequestQueryBody<any, TeamsRequestBody>,
res: NextApiResponse<Team[] | Team>,
) => {
await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
if (req.method === 'GET') {
const teams = await getUserTeams(userId);
return ok(res, teams);
}
if (req.method === 'POST') {
if (!(await canCreateTeam(req.auth))) {
return unauthorized(res);
}
const { name } = req.body;
const team = await createTeam(
{
id: uuid(),
name,
accessCode: getRandomChars(16),
},
userId,
);
return ok(res, team);
}
return methodNotAllowed(res);
};

View File

@ -1,34 +0,0 @@
import { useAuth, useCors } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getUserWebsites } from 'queries';
export interface WebsitesRequestBody {
name: string;
domain: string;
shareId: string;
}
export default async (
req: NextApiRequestQueryBody<any, WebsitesRequestBody>,
res: NextApiResponse,
) => {
await useCors(req, res);
await useAuth(req, res);
const { user } = req.auth;
const { id: userId } = req.query;
if (req.method === 'GET') {
if (!user.isAdmin && user.id !== userId) {
return unauthorized(res);
}
const websites = await getUserWebsites(userId);
return ok(res, websites);
}
return methodNotAllowed(res);
};

View File

@ -1,60 +0,0 @@
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors } from 'lib/middleware';
import { NextApiRequestQueryBody, WebsiteEventDataMetric } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getEventData } from 'queries';
export interface WebsiteEventDataRequestQuery {
id: string;
}
export interface WebsiteEventDataRequestBody {
startAt: string;
endAt: string;
eventName?: string;
urlPath?: string;
timeSeries?: {
unit: string;
timezone: string;
};
filters: [
{
eventKey?: string;
eventValue?: string | number | boolean | Date;
},
];
}
export default async (
req: NextApiRequestQueryBody<WebsiteEventDataRequestQuery, WebsiteEventDataRequestBody>,
res: NextApiResponse<WebsiteEventDataMetric[]>,
) => {
await useCors(req, res);
await useAuth(req, res);
const { id: websiteId } = req.query;
if (req.method === 'POST') {
if (!(await canViewWebsite(req.auth, websiteId))) {
return unauthorized(res);
}
const { startAt, endAt, eventName, urlPath, filters } = req.body;
const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
const events = await getEventData(websiteId, {
startDate,
endDate,
eventName,
urlPath,
filters,
});
return ok(res, events);
}
return methodNotAllowed(res);
};

View File

@ -1,54 +0,0 @@
import { canCreateWebsite } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth, useCors } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createWebsite, getUserWebsites } from 'queries';
export interface WebsitesRequestBody {
name: string;
domain: string;
shareId: string;
}
export default async (
req: NextApiRequestQueryBody<any, WebsitesRequestBody>,
res: NextApiResponse,
) => {
await useCors(req, res);
await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
if (req.method === 'GET') {
const websites = await getUserWebsites(userId);
return ok(res, websites);
}
if (req.method === 'POST') {
const { name, domain, shareId } = req.body;
if (!(await canCreateWebsite(req.auth))) {
return unauthorized(res);
}
const data: any = {
id: uuid(),
name,
domain,
shareId,
};
data.userId = userId;
const website = await createWebsite(data);
return ok(res, website);
}
return methodNotAllowed(res);
};

View File

@ -1,26 +0,0 @@
import { useRouter } from 'next/router';
import AppLayout from 'components/layout/AppLayout';
import RealtimeDashboard from 'components/pages/realtime/RealtimeDashboard';
import useMessages from 'hooks/useMessages';
import useApi from 'hooks/useApi';
export default function RealtimeDetailsPage() {
const router = useRouter();
const { id: websiteId } = router.query;
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const { data: website } = useQuery(['websites', websiteId], () =>
get(`/websites/${websiteId}`, { enabled: !!websiteId }),
);
const title = `${formatMessage(labels.realtime)}${website?.name ? ` - ${website.name}` : ''}`;
if (!websiteId) {
return null;
}
return (
<AppLayout title={title}>
<RealtimeDashboard key={websiteId} websiteId={websiteId} />
</AppLayout>
);
}

View File

@ -1,12 +0,0 @@
import AppLayout from 'components/layout/AppLayout';
import RealtimeHome from 'components/pages/realtime/RealtimeHome';
import useMessages from 'hooks/useMessages';
export default function RealtimePage() {
const { formatMessage, labels } = useMessages();
return (
<AppLayout title={formatMessage(labels.realtime)}>
<RealtimeHome />
</AppLayout>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More