Merge branch 'master' of github.com:umami-software/umami
@ -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"
|
||||
|
5
.github/ISSUE_TEMPLATE/2.feature_request.yml
vendored
@ -1,10 +1,9 @@
|
||||
name: "✨ Feature Request"
|
||||
name: '✨ Feature Request'
|
||||
description: Create a feature or enhancement request for Umami.
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature or enhancement
|
||||
description: A clear and concise description of what the feature or enhancement is.
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
|
19
.github/stale.yml
vendored
@ -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
|
12
.github/workflows/cd-manual.yml
vendored
@ -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 }}
|
12
.github/workflows/cd.yml
vendored
@ -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 }}
|
43
.github/workflows/ci.yml
vendored
@ -16,27 +16,24 @@ 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
|
||||
db-type: mysql
|
||||
- node-version: 18.x
|
||||
db-type: postgresql
|
||||
- node-version: 18.x
|
||||
db-type: mysql
|
||||
- node-version: 16.x
|
||||
db-type: postgresql
|
||||
- node-version: 16.x
|
||||
db-type: mysql
|
||||
- node-version: 18.x
|
||||
db-type: postgresql
|
||||
- node-version: 18.x
|
||||
db-type: mysql
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
env:
|
||||
DATABASE_TYPE: ${{ matrix.db-type }}
|
||||
- run: npm install --global yarn
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
env:
|
||||
DATABASE_TYPE: ${{ matrix.db-type }}
|
||||
- run: npm install --global yarn
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
|
24
.github/workflows/stale-issues.yml
vendored
Normal 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
@ -25,6 +25,7 @@ node_modules
|
||||
*.iml
|
||||
*.log
|
||||
.vscode
|
||||
.tool-versions
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
@ -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 |
@ -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 |
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
18
db/clickhouse/migrations/01_edit_keys.sql
Normal 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
|
@ -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;
|
||||
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;
|
@ -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;
|
@ -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`);
|
@ -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,20 +101,25 @@ 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)
|
||||
websiteId String @map("website_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
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
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)
|
||||
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])
|
||||
websiteEvent WebsiteEvent @relation(fields: [websiteEventId], 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")
|
||||
}
|
||||
|
@ -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;
|
@ -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");
|
@ -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,20 +101,25 @@ 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
|
||||
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
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
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)
|
||||
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])
|
||||
websiteEvent WebsiteEvent @relation(fields: [websiteEventId], 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")
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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];
|
||||
}
|
@ -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];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
"baseUrl": "./src"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
120
lang/am-ET.json
@ -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"
|
||||
}
|
146
lang/de-CH.json
@ -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."
|
||||
}
|
120
lang/hr-HR.json
@ -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"
|
||||
}
|
146
lang/ja-JP.json
@ -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."
|
||||
}
|
146
lang/mn-MN.json
@ -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."
|
||||
}
|
146
lang/nl-NL.json
@ -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."
|
||||
}
|
146
lang/pt-BR.json
@ -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."
|
||||
}
|
120
lang/si-LK.json
@ -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": "අමුත්තන්"
|
||||
}
|
146
lang/sv-SE.json
@ -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."
|
||||
}
|
146
lang/zh-TW.json
@ -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."
|
||||
}
|
162
lib/prisma.ts
@ -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,
|
||||
};
|
131
lib/types.ts
@ -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;
|
||||
}
|
@ -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
@ -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"
|
||||
}
|
||||
}
|
56
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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);
|
||||
};
|
@ -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);
|
||||
};
|
@ -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);
|
||||
};
|
@ -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);
|
||||
};
|
@ -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);
|
||||
};
|
@ -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);
|
||||
};
|
@ -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);
|
||||
};
|
@ -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);
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
BIN
public/images/browsers/android-webview.png
Normal file
After Width: | Height: | Size: 806 B |
BIN
public/images/browsers/android.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/browsers/aol.png
Normal file
After Width: | Height: | Size: 420 B |
BIN
public/images/browsers/beaker.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/browsers/blackberry.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/browsers/brave.png
Normal file
After Width: | Height: | Size: 635 B |
BIN
public/images/browsers/chrome.png
Normal file
After Width: | Height: | Size: 819 B |
BIN
public/images/browsers/chromium-webview.png
Normal file
After Width: | Height: | Size: 680 B |
BIN
public/images/browsers/crios.png
Normal file
After Width: | Height: | Size: 819 B |
BIN
public/images/browsers/curl.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/browsers/edge-chromium.png
Normal file
After Width: | Height: | Size: 811 B |
BIN
public/images/browsers/edge-ios.png
Normal file
After Width: | Height: | Size: 811 B |
BIN
public/images/browsers/edge.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/browsers/facebook.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/browsers/firefox.png
Normal file
After Width: | Height: | Size: 835 B |
BIN
public/images/browsers/fxios.png
Normal file
After Width: | Height: | Size: 835 B |
BIN
public/images/browsers/ie.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/browsers/instagram.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/browsers/ios-webview.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/browsers/ios.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/browsers/kakaotalk.png
Normal file
After Width: | Height: | Size: 426 B |
BIN
public/images/browsers/miui.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/images/browsers/opera-mini.png
Normal file
After Width: | Height: | Size: 794 B |
BIN
public/images/browsers/opera.png
Normal file
After Width: | Height: | Size: 632 B |
BIN
public/images/browsers/safari.png
Normal file
After Width: | Height: | Size: 829 B |
BIN
public/images/browsers/samsung.png
Normal file
After Width: | Height: | Size: 535 B |
BIN
public/images/browsers/searchbot.png
Normal file
After Width: | Height: | Size: 1.2 KiB |