mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-07 18:08:04 +01:00
Merge branch 'dev' into analytics
This commit is contained in:
commit
b0ba08f16b
19
.github/stale.yml
vendored
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
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -16,13 +16,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- node-version: 14.x
|
||||
- node-version: 16.x
|
||||
db-type: postgresql
|
||||
- node-version: 14.x
|
||||
- node-version: 16.x
|
||||
db-type: mysql
|
||||
- node-version: 16.x
|
||||
- node-version: 18.x
|
||||
db-type: postgresql
|
||||
- node-version: 16.x
|
||||
- node-version: 18.x
|
||||
db-type: mysql
|
||||
|
||||
steps:
|
||||
|
22
.github/workflows/stale-issues.yml
vendored
Normal file
22
.github/workflows/stale-issues.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
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
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,6 +25,7 @@ node_modules
|
||||
*.iml
|
||||
*.log
|
||||
.vscode
|
||||
.tool-versions
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
1
assets/bar-chart.svg
Normal file
1
assets/bar-chart.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m7 13v9a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1-1v-9a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1zm7-12h-4a1 1 0 0 0 -1 1v20a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-20a1 1 0 0 0 -1-1zm8 5h-4a1 1 0 0 0 -1 1v15a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-15a1 1 0 0 0 -1-1z"/></svg>
|
After Width: | Height: | Size: 328 B |
@ -1,15 +1,23 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { Button, Row, Column } from 'react-basics';
|
||||
import { setItem } from 'next-basics';
|
||||
import useStore, { checkVersion } from 'store/version';
|
||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||
import styles from './UpdateNotice.module.css';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export function UpdateNotice() {
|
||||
export function UpdateNotice({ user, config }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { latest, checked, hasUpdate, releaseUrl } = useStore();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const { pathname } = useRouter();
|
||||
const [dismissed, setDismissed] = useState(checked);
|
||||
const allowUpdate =
|
||||
user?.isAdmin &&
|
||||
!config?.updatesDisabled &&
|
||||
!config?.cloudMode &&
|
||||
!pathname.includes('/share/') &&
|
||||
!dismissed;
|
||||
|
||||
const updateCheck = useCallback(() => {
|
||||
setItem(VERSION_CHECK, { version: latest, time: Date.now() });
|
||||
@ -27,12 +35,12 @@ export function UpdateNotice() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!checked) {
|
||||
if (allowUpdate) {
|
||||
checkVersion();
|
||||
}
|
||||
}, [checked]);
|
||||
}, [allowUpdate]);
|
||||
|
||||
if (!hasUpdate || dismissed) {
|
||||
if (!allowUpdate || !hasUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
gap: 20px;
|
||||
margin: 20px auto;
|
||||
justify-self: center;
|
||||
background: #fff;
|
||||
background: var(--base50);
|
||||
padding: 20px;
|
||||
border: 1px solid var(--base300);
|
||||
border-radius: var(--border-radius);
|
||||
@ -15,7 +15,8 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: var(--font-color100);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Icons } from 'react-basics';
|
||||
import AddUser from 'assets/add-user.svg';
|
||||
import Bars from 'assets/bars.svg';
|
||||
import BarChart from 'assets/bar-chart.svg';
|
||||
import Bolt from 'assets/bolt.svg';
|
||||
import Calendar from 'assets/calendar.svg';
|
||||
import Clock from 'assets/clock.svg';
|
||||
@ -22,6 +24,8 @@ import Visitor from 'assets/visitor.svg';
|
||||
const icons = {
|
||||
...Icons,
|
||||
AddUser,
|
||||
Bars,
|
||||
BarChart,
|
||||
Bolt,
|
||||
Calendar,
|
||||
Clock,
|
||||
|
@ -10,11 +10,7 @@ export function RefreshButton({ websiteId, isLoading }) {
|
||||
|
||||
function handleClick() {
|
||||
if (!isLoading && dateRange) {
|
||||
if (/^\d+/.test(dateRange.value)) {
|
||||
setWebsiteDateRange(websiteId, dateRange.value);
|
||||
} else {
|
||||
setWebsiteDateRange(websiteId, dateRange);
|
||||
}
|
||||
setWebsiteDateRange(websiteId, dateRange);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,25 +1,13 @@
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import DateFilter from './DateFilter';
|
||||
import styles from './WebsiteDateFilter.module.css';
|
||||
|
||||
export default function WebsiteDateFilter({ websiteId }) {
|
||||
const { get } = useApi();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { value, startDate, endDate } = dateRange;
|
||||
|
||||
const handleChange = async value => {
|
||||
if (value === 'all' && websiteId) {
|
||||
const data = await get(`/websites/${websiteId}`);
|
||||
|
||||
if (data) {
|
||||
const start = new Date(data.createdAt).getTime();
|
||||
const end = Date.now();
|
||||
setDateRange(`range:${start}:${end}`);
|
||||
}
|
||||
} else if (value !== 'all') {
|
||||
setDateRange(value);
|
||||
}
|
||||
setDateRange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -11,17 +11,14 @@ import styles from './AppLayout.module.css';
|
||||
export function AppLayout({ title, children }) {
|
||||
const { user } = useRequireLogin();
|
||||
const config = useConfig();
|
||||
const { pathname } = useRouter();
|
||||
|
||||
if (!user || !config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allowUpdate = user?.isAdmin && !config?.updatesDisabled && !pathname.includes('/share/');
|
||||
|
||||
return (
|
||||
<div className={styles.layout} data-app-version={CURRENT_VERSION}>
|
||||
{allowUpdate && <UpdateNotice />}
|
||||
<UpdateNotice user={user} config={config} />
|
||||
<Head>
|
||||
<title>{title ? `${title} | umami` : 'umami'}</title>
|
||||
</Head>
|
||||
|
@ -8,7 +8,6 @@
|
||||
.nav {
|
||||
height: 60px;
|
||||
width: 100vw;
|
||||
z-index: var(--z-index-overlay);
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ export const labels = defineMessages({
|
||||
devices: { id: 'label.devices', defaultMessage: 'Devices' },
|
||||
countries: { id: 'label.countries', defaultMessage: 'Countries' },
|
||||
languages: { id: 'label.languages', defaultMessage: 'Languages' },
|
||||
event: { id: 'label.event', defaultMessage: 'Event' },
|
||||
events: { id: 'label.events', defaultMessage: 'Events' },
|
||||
query: { id: 'label.query', defaultMessage: 'Query' },
|
||||
queryParameters: { id: 'label.query-parameters', defaultMessage: 'Query parameters' },
|
||||
@ -159,6 +160,8 @@ export const labels = defineMessages({
|
||||
value: { id: 'labels.value', defaultMessage: 'Value' },
|
||||
overview: { id: 'labels.overview', defaultMessage: 'Overview' },
|
||||
totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' },
|
||||
insights: { id: 'label.insights', defaultMessage: 'Insights' },
|
||||
dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
@ -270,4 +273,8 @@ export const messages = defineMessages({
|
||||
id: 'message.no-event-data',
|
||||
defaultMessage: 'No event data is available.',
|
||||
},
|
||||
newVersionAvailable: {
|
||||
id: 'new-version-available',
|
||||
defaultMessage: 'A new version of Umami {version} is available!',
|
||||
},
|
||||
});
|
||||
|
@ -29,7 +29,7 @@ export function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusLight variant="success">
|
||||
<StatusLight className={styles.container} variant="success">
|
||||
<div className={styles.text}>{formatMessage(messages.activeUsers, { x: count })}</div>
|
||||
</StatusLight>
|
||||
);
|
||||
|
@ -1,10 +1,14 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.value {
|
||||
|
@ -3,7 +3,7 @@ import BarChart from './BarChart';
|
||||
import { useLocale, useTheme, useMessages } from 'hooks';
|
||||
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
|
||||
|
||||
export function PageviewsChart({ websiteId, data, unit, className, loading, ...props }) {
|
||||
export function PageviewsChart({ websiteId, data, unit, loading, ...props }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { colors } = useTheme();
|
||||
const { locale } = useLocale();
|
||||
@ -31,7 +31,6 @@ export function PageviewsChart({ websiteId, data, unit, className, loading, ...p
|
||||
<BarChart
|
||||
{...props}
|
||||
key={websiteId}
|
||||
className={className}
|
||||
datasets={datasets}
|
||||
unit={unit}
|
||||
loading={loading}
|
||||
|
@ -18,7 +18,9 @@ export function Dashboard({ userId }) {
|
||||
const { showCharts, limit, editing } = dashboard;
|
||||
const [max, setMax] = useState(limit);
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['websites'], () => get('/websites', { userId }));
|
||||
const { data, isLoading, error } = useQuery(['websites'], () =>
|
||||
get('/websites', { userId, includeTeams: 1 }),
|
||||
);
|
||||
const hasData = data && data.length !== 0;
|
||||
const { dir } = useLocale();
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Menu, Icon, Text, PopupTrigger, Popup, Item, Button } from 'react-basics';
|
||||
import { TooltipPopup, Icon, Text, Flexbox, Popup, Item, Button } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import { saveDashboard } from 'store/dashboard';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
@ -6,40 +6,30 @@ 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',
|
||||
},
|
||||
];
|
||||
const handleToggleCharts = () => {
|
||||
saveDashboard(state => ({ showCharts: !state.showCharts }));
|
||||
};
|
||||
|
||||
function handleSelect(value) {
|
||||
if (value === 'charts') {
|
||||
saveDashboard(state => ({ showCharts: !state.showCharts }));
|
||||
}
|
||||
if (value === 'order') {
|
||||
saveDashboard({ editing: true });
|
||||
}
|
||||
}
|
||||
const handleEdit = () => {
|
||||
saveDashboard({ editing: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button>
|
||||
<Flexbox gap={10}>
|
||||
<TooltipPopup label={formatMessage(labels.toggleCharts)} position="bottom">
|
||||
<Button onClick={handleToggleCharts}>
|
||||
<Icon>
|
||||
<Icons.BarChart />
|
||||
</Icon>
|
||||
</Button>
|
||||
</TooltipPopup>
|
||||
<Button onClick={handleEdit}>
|
||||
<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>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -13,14 +13,15 @@ export function EventDataTable({ data = [] }) {
|
||||
|
||||
return (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="event" label={formatMessage(labels.event)}>
|
||||
{row => (
|
||||
<Link href={resolveUrl({ event: row.event })} shallow={true}>
|
||||
{row.event}
|
||||
</Link>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="field" label={formatMessage(labels.field)}>
|
||||
{row => {
|
||||
return (
|
||||
<Link href={resolveUrl({ view: row.field })} shallow={true}>
|
||||
{row.field}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
{row => row.field}
|
||||
</GridColumn>
|
||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)}>
|
||||
{({ total }) => total.toLocaleString()}
|
||||
|
@ -5,14 +5,14 @@ import Icons from 'components/icons';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Empty from 'components/common/Empty';
|
||||
|
||||
export function EventDataTable({ data = [], field }) {
|
||||
export function EventDataValueTable({ data = [], event }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { resolveUrl } = usePageQuery();
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<>
|
||||
<Link href={resolveUrl({ view: undefined })}>
|
||||
<Link href={resolveUrl({ event: undefined })}>
|
||||
<Button>
|
||||
<Icon rotate={180}>
|
||||
<Icons.ArrowRight />
|
||||
@ -20,7 +20,7 @@ export function EventDataTable({ data = [], field }) {
|
||||
<Text>{formatMessage(labels.back)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
<Text>{field}</Text>
|
||||
<Text>{event}</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -31,6 +31,7 @@ export function EventDataTable({ data = [], field }) {
|
||||
{data.length <= 0 && <Empty />}
|
||||
{data.length > 0 && (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="field" label={formatMessage(labels.field)} />
|
||||
<GridColumn name="value" label={formatMessage(labels.value)} />
|
||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)} width="200px">
|
||||
{({ total }) => total.toLocaleString()}
|
||||
@ -41,4 +42,4 @@ export function EventDataTable({ data = [], field }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDataTable;
|
||||
export default EventDataValueTable;
|
||||
|
@ -1,17 +1,26 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import DataTable from 'components/metrics/DataTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import classNames from 'classnames';
|
||||
import styles from './RealtimeCountries.module.css';
|
||||
|
||||
export function RealtimeCountries({ data }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { basePath } = useRouter();
|
||||
|
||||
const renderCountryName = useCallback(
|
||||
({ x }) => <span className={locale}>{countryNames[x]}</span>,
|
||||
[countryNames, locale],
|
||||
({ x: code }) => (
|
||||
<span className={classNames(locale, styles.row)}>
|
||||
<img src={`${basePath}/images/flags/${code?.toLowerCase() || 'xx'}.png`} alt={code} />
|
||||
{countryNames[code]}
|
||||
</span>
|
||||
),
|
||||
[countryNames, locale, basePath],
|
||||
);
|
||||
|
||||
return (
|
||||
|
5
components/pages/realtime/RealtimeCountries.module.css
Normal file
5
components/pages/realtime/RealtimeCountries.module.css
Normal file
@ -0,0 +1,5 @@
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
@ -52,7 +52,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
||||
|
||||
const getTime = ({ createdAt }) => dateFormat(new Date(createdAt), 'pp', locale);
|
||||
|
||||
const getColor = ({ sessionId }) => stringToColor(sessionId);
|
||||
const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
|
||||
|
||||
const getIcon = ({ __type }) => icons[__type];
|
||||
|
||||
|
@ -93,9 +93,7 @@ export function RealtimePage({ websiteId }) {
|
||||
<Page loading={isLoading} error={error}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<RealtimeHeader websiteId={websiteId} data={currentData} />
|
||||
<div className={styles.chart}>
|
||||
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
|
||||
</div>
|
||||
<RealtimeChart className={styles.chart} data={realtimeData} unit="minute" />
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} lg={4} xl={4}>
|
||||
<RealtimeUrls websiteId={websiteId} websiteDomain={website?.domain} data={realtimeData} />
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { REPORT_PARAMETERS } from 'lib/constants';
|
||||
import PopupForm from '../PopupForm';
|
||||
import FieldSelectForm from '../FieldSelectForm';
|
||||
import FieldAggregateForm from '../FieldAggregateForm';
|
||||
import FieldFilterForm from '../FieldFilterForm';
|
||||
import PopupForm from './PopupForm';
|
||||
import FieldSelectForm from './FieldSelectForm';
|
||||
import FieldAggregateForm from './FieldAggregateForm';
|
||||
import FieldFilterForm from './FieldFilterForm';
|
||||
import styles from './FieldAddForm.module.css';
|
||||
|
||||
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
@ -19,6 +19,10 @@ export default function FieldAggregateForm({ name, type, onSelect }) {
|
||||
{ label: formatMessage(labels.total), value: 'total' },
|
||||
{ label: formatMessage(labels.unique), value: 'unique' },
|
||||
],
|
||||
uuid: [
|
||||
{ label: formatMessage(labels.total), value: 'total' },
|
||||
{ label: formatMessage(labels.unique), value: 'unique' },
|
||||
],
|
||||
};
|
||||
|
||||
const items = options[type];
|
||||
|
@ -9,10 +9,10 @@ export default function FieldSelectForm({ fields, onSelect }) {
|
||||
<Form>
|
||||
<FormRow label={formatMessage(labels.fields)}>
|
||||
<Menu className={styles.menu} onSelect={key => onSelect(fields[key])}>
|
||||
{fields.map(({ name, type }, index) => {
|
||||
{fields.map(({ label, name, type }, index) => {
|
||||
return (
|
||||
<Item key={index} className={styles.item}>
|
||||
<div>{name}</div>
|
||||
<div>{label || name}</div>
|
||||
<div className={styles.type}>{type}</div>
|
||||
</Item>
|
||||
);
|
||||
|
@ -3,20 +3,10 @@ import { Button, Icons, Text, Icon } from 'react-basics';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
import styles from './ReportTemplates.module.css';
|
||||
import { useMessages } from 'hooks';
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: 'Funnel',
|
||||
description: 'Understand the conversion and drop-off rate of users.',
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
];
|
||||
|
||||
function ReportItem({ title, description, url, icon }) {
|
||||
return (
|
||||
<div className={styles.report}>
|
||||
@ -42,6 +32,21 @@ function ReportItem({ title, description, url, icon }) {
|
||||
export function ReportTemplates() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: formatMessage(labels.insights),
|
||||
description: 'Dive deeper into your data by using segments and filters.',
|
||||
url: '/reports/insights',
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.funnel),
|
||||
description: 'Understand the conversion and drop-off rate of users.',
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader title={formatMessage(labels.reports)} />
|
||||
|
@ -5,7 +5,7 @@ import { ReportContext } from 'components/pages/reports/Report';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import Icons from 'components/icons';
|
||||
import FieldAddForm from './FieldAddForm';
|
||||
import FieldAddForm from '../FieldAddForm';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import ParameterList from '../ParameterList';
|
||||
import styles from './EventDataParameters.module.css';
|
||||
@ -54,9 +54,11 @@ export function EventDataParameters() {
|
||||
};
|
||||
|
||||
const handleAdd = (group, value) => {
|
||||
const data = parameterData[group].filter(({ name }) => name !== value.name);
|
||||
const data = parameterData[group];
|
||||
|
||||
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||
if (!data.find(({ name }) => name === value.name)) {
|
||||
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (group, index) => {
|
||||
|
@ -6,13 +6,12 @@ import { ReportContext } from '../Report';
|
||||
export function FunnelTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data={report?.data}
|
||||
title={formatMessage(labels.url)}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
showPercentage={false}
|
||||
showPercentage={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { REPORT_PARAMETERS } from 'lib/constants';
|
||||
import PopupForm from '../PopupForm';
|
||||
import FieldSelectForm from '../FieldSelectForm';
|
||||
import FieldAggregateForm from '../FieldAggregateForm';
|
||||
import FieldFilterForm from '../FieldFilterForm';
|
||||
import styles from './FieldAddForm.module.css';
|
||||
|
||||
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
||||
const [selected, setSelected] = useState();
|
||||
|
||||
const handleSelect = value => {
|
||||
const { type } = value;
|
||||
|
||||
if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
|
||||
value.value = group === REPORT_PARAMETERS.groups ? '' : 'total';
|
||||
handleSave(value);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelected(value);
|
||||
};
|
||||
|
||||
const handleSave = value => {
|
||||
onAdd(group, value);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<PopupForm className={styles.popup} element={element} onClose={onClose}>
|
||||
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
||||
{selected && group === REPORT_PARAMETERS.fields && (
|
||||
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
||||
)}
|
||||
{selected && group === REPORT_PARAMETERS.filters && (
|
||||
<FieldFilterForm {...selected} onSelect={handleSave} />
|
||||
)}
|
||||
</PopupForm>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldAddForm;
|
@ -1,38 +0,0 @@
|
||||
.menu {
|
||||
width: 360px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.type {
|
||||
color: var(--font-color300);
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 60px;
|
||||
}
|
@ -1,42 +1,22 @@
|
||||
import { useContext, useRef } from 'react';
|
||||
import { useApi, useMessages } from 'hooks';
|
||||
import { useMessages } from 'hooks';
|
||||
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import { REPORT_PARAMETERS, WEBSITE_EVENT_FIELDS } from 'lib/constants';
|
||||
import Icons from 'components/icons';
|
||||
import FieldAddForm from './FieldAddForm';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import FieldAddForm from '../FieldAddForm';
|
||||
import ParameterList from '../ParameterList';
|
||||
import styles from './InsightsParameters.module.css';
|
||||
|
||||
function useFields(websiteId, startDate, endDate) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['fields', websiteId, startDate, endDate],
|
||||
() =>
|
||||
get('/reports/event-data', {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
}),
|
||||
{ enabled: !!(websiteId && startDate && endDate) },
|
||||
);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function InsightsParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const ref = useRef(null);
|
||||
const { parameters } = report || {};
|
||||
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
|
||||
const { startDate, endDate } = dateRange || {};
|
||||
const queryEnabled = websiteId && dateRange && fields?.length;
|
||||
const { data, error } = useFields(websiteId, startDate, endDate);
|
||||
const parametersSelected = websiteId && startDate && endDate;
|
||||
const hasData = data?.length !== 0;
|
||||
const fieldOptions = Object.keys(WEBSITE_EVENT_FIELDS).map(key => WEBSITE_EVENT_FIELDS[key]);
|
||||
|
||||
const parameterGroups = [
|
||||
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
||||
@ -78,10 +58,7 @@ export function InsightsParameters() {
|
||||
{(close, element) => {
|
||||
return (
|
||||
<FieldAddForm
|
||||
fields={data.map(({ eventKey, InsightsType }) => ({
|
||||
name: eventKey,
|
||||
type: DATA_TYPES[InsightsType],
|
||||
}))}
|
||||
fields={fieldOptions}
|
||||
group={group}
|
||||
element={element}
|
||||
onAdd={handleAdd}
|
||||
@ -95,50 +72,43 @@ export function InsightsParameters() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Form ref={ref} values={parameters} error={error} onSubmit={handleSubmit}>
|
||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
|
||||
<BaseParameters />
|
||||
{!hasData && <Empty message={formatMessage(messages.noInsights)} />}
|
||||
{parametersSelected &&
|
||||
hasData &&
|
||||
parameterGroups.map(({ label, group }) => {
|
||||
return (
|
||||
<FormRow
|
||||
key={label}
|
||||
label={label}
|
||||
action={<AddButton group={group} onAdd={handleAdd} />}
|
||||
{parameterGroups.map(({ label, group }) => {
|
||||
return (
|
||||
<FormRow key={label} label={label} action={<AddButton group={group} onAdd={handleAdd} />}>
|
||||
<ParameterList
|
||||
items={parameterData[group]}
|
||||
onRemove={index => handleRemove(group, index)}
|
||||
>
|
||||
<ParameterList
|
||||
items={parameterData[group]}
|
||||
onRemove={index => handleRemove(group, index)}
|
||||
>
|
||||
{({ name, value }) => {
|
||||
return (
|
||||
<div className={styles.parameter}>
|
||||
{group === REPORT_PARAMETERS.fields && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.filters && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value[0]}</div>
|
||||
<div>{value[1]}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.groups && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
);
|
||||
})}
|
||||
{({ name, value }) => {
|
||||
return (
|
||||
<div className={styles.parameter}>
|
||||
{group === REPORT_PARAMETERS.fields && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.filters && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value[0]}</div>
|
||||
<div>{value[1]}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.groups && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
);
|
||||
})}
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={!queryEnabled} loading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
} from 'react-basics';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getRandomChars } from 'next-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
@ -20,12 +21,16 @@ export function ShareUrl({ websiteId, data, onSave }) {
|
||||
const { name, shareId } = data;
|
||||
const [id, setId] = useState(shareId);
|
||||
const { post, useMutation } = useApi();
|
||||
const { basePath } = useRouter();
|
||||
const { mutate, error } = useMutation(({ shareId }) =>
|
||||
post(`/websites/${websiteId}`, { shareId }),
|
||||
);
|
||||
const ref = useRef(null);
|
||||
const url = useMemo(
|
||||
() => `${process.env.analyticsUrl || location.origin}/share/${id}/${encodeURIComponent(name)}`,
|
||||
() =>
|
||||
`${process.env.analyticsUrl || location.origin}${basePath}/share/${id}/${encodeURIComponent(
|
||||
name,
|
||||
)}`,
|
||||
[id, name],
|
||||
);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import PageviewsChart from 'components/metrics/PageviewsChart';
|
||||
import { useApi, useDateRange, useTimezone, usePageQuery } from 'hooks';
|
||||
import { getDateArray, getDateLength } from 'lib/date';
|
||||
import { getDateArray } from 'lib/date';
|
||||
|
||||
export function WebsiteChart({ websiteId }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
@ -43,17 +43,9 @@ export function WebsiteChart({ websiteId }) {
|
||||
};
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data, startDate, endDate, unit, modified]);
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
return (
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
loading={isLoading}
|
||||
/>
|
||||
);
|
||||
return <PageviewsChart websiteId={websiteId} data={chartData} unit={unit} loading={isLoading} />;
|
||||
}
|
||||
|
||||
export default WebsiteChart;
|
||||
|
@ -41,7 +41,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
|
||||
</Link>
|
||||
</WebsiteHeader>
|
||||
<WebsiteMetricsBar websiteId={id} />
|
||||
<WebsiteChart websiteId={id} showChart={showCharts} />
|
||||
{showCharts && <WebsiteChart websiteId={id} />}
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
|
@ -5,18 +5,18 @@ import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetric
|
||||
import { useDateRange, useApi, usePageQuery } from 'hooks';
|
||||
import styles from './WebsiteEventData.module.css';
|
||||
|
||||
function useFields(websiteId, field) {
|
||||
function useData(websiteId, event) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['event-data:fields', { websiteId, startDate, endDate, field }],
|
||||
['event-data:events', { websiteId, startDate, endDate, event }],
|
||||
() =>
|
||||
get('/event-data/fields', {
|
||||
get('/event-data/events', {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
field,
|
||||
event,
|
||||
}),
|
||||
{ enabled: !!(websiteId && startDate && endDate) },
|
||||
);
|
||||
@ -26,15 +26,15 @@ function useFields(websiteId, field) {
|
||||
|
||||
export default function WebsiteEventData({ websiteId }) {
|
||||
const {
|
||||
query: { view },
|
||||
query: { event },
|
||||
} = usePageQuery();
|
||||
const { data } = useFields(websiteId, view);
|
||||
const { data } = useData(websiteId, event);
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container} direction="column" gap={20}>
|
||||
<EventDataMetricsBar websiteId={websiteId} />
|
||||
{!view && <EventDataTable data={data} />}
|
||||
{view && <EventDataValueTable field={view} data={data} />}
|
||||
{!event && <EventDataTable data={data} />}
|
||||
{event && <EventDataValueTable event={event} data={data} />}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import ActiveUsers from 'components/metrics/ActiveUsers';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
import Icons from 'components/icons';
|
||||
import { useMessages, useWebsite } from 'hooks';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export function WebsiteHeader({ websiteId, showLinks = true, children }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
@ -42,11 +42,11 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) {
|
||||
<Column className={styles.title} variant="two">
|
||||
<Favicon domain={domain} />
|
||||
<Text>{name}</Text>
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column className={styles.actions} variant="two">
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
{showLinks && (
|
||||
<Flexbox alignItems="center">
|
||||
<div className={styles.links}>
|
||||
{links.map(({ label, icon, path }) => {
|
||||
const selected = path ? pathname.endsWith(path) : pathname === '/websites/[id]';
|
||||
|
||||
@ -58,13 +58,13 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) {
|
||||
[styles.selected]: selected,
|
||||
})}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
<Icon className={styles.icon}>{icon}</Icon>
|
||||
<Text className={styles.label}>{label}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Flexbox>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</Column>
|
||||
|
@ -27,3 +27,29 @@
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.links {
|
||||
justify-content: space-evenly;
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.icon svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
@ -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,14 +17,14 @@ 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'),
|
||||
@ -38,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),
|
||||
@ -49,14 +49,14 @@ 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'),
|
||||
@ -66,11 +66,11 @@ CREATE TABLE umami.website_event_queue (
|
||||
)
|
||||
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_handle_error_mode = 'stream'
|
||||
kafka_handle_error_mode = 'stream';
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.website_event_queue_mv TO umami.website_event AS
|
||||
SELECT website_id,
|
||||
@ -108,7 +108,7 @@ SETTINGS index_granularity = 8192 AS
|
||||
SELECT _error AS error,
|
||||
_raw_message AS raw
|
||||
FROM umami.website_event_queue
|
||||
WHERE length(_error) > 0
|
||||
WHERE length(_error) > 0;
|
||||
|
||||
CREATE TABLE umami.event_data
|
||||
(
|
||||
@ -151,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_handle_error_mode = 'stream'
|
||||
kafka_handle_error_mode = 'stream';
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.event_data_queue_mv TO umami.event_data AS
|
||||
SELECT website_id,
|
||||
@ -178,4 +178,4 @@ SETTINGS index_granularity = 8192 AS
|
||||
SELECT _error AS error,
|
||||
_raw_message AS raw
|
||||
FROM umami.event_data_queue
|
||||
WHERE length(_error) > 0
|
||||
WHERE length(_error) > 0;
|
@ -97,8 +97,8 @@ model WebsiteEvent {
|
||||
|
||||
model EventData {
|
||||
id String @id() @map("event_data_id") @db.VarChar(36)
|
||||
websiteEventId String @map("website_event_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)
|
||||
|
@ -21,7 +21,7 @@ export function useConfig() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
return config || {};
|
||||
return config;
|
||||
}
|
||||
|
||||
export default useConfig;
|
||||
|
@ -1,20 +1,43 @@
|
||||
import { parseDateRange } from 'lib/date';
|
||||
import { getMinimumUnit, 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';
|
||||
import useApi from './useApi';
|
||||
|
||||
export function useDateRange(websiteId) {
|
||||
const { get } = useApi();
|
||||
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);
|
||||
|
||||
const saveDateRange = value => {
|
||||
const saveDateRange = async value => {
|
||||
if (websiteId) {
|
||||
setWebsiteDateRange(websiteId, value);
|
||||
let dateRange = value;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (value === 'all') {
|
||||
const result = await get(`/websites/${websiteId}/daterange`);
|
||||
const { mindate, maxdate } = result;
|
||||
|
||||
const startDate = new Date(mindate);
|
||||
const endDate = new Date(maxdate);
|
||||
|
||||
dateRange = {
|
||||
startDate,
|
||||
endDate,
|
||||
unit: getMinimumUnit(startDate, endDate),
|
||||
value,
|
||||
};
|
||||
} else {
|
||||
dateRange = parseDateRange(value, locale);
|
||||
}
|
||||
}
|
||||
|
||||
setWebsiteDateRange(websiteId, dateRange);
|
||||
} else {
|
||||
setItem(DATE_RANGE_CONFIG, value);
|
||||
setDateRange(value);
|
||||
|
@ -24,6 +24,7 @@ export function useFilters() {
|
||||
boolean: ['t', 'f'],
|
||||
number: ['eq', 'neq', 'gt', 'lt', 'gte', 'lte'],
|
||||
date: ['be', 'af'],
|
||||
uuid: ['eq'],
|
||||
};
|
||||
|
||||
return { filters, types };
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Edit",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Enable share URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event Data",
|
||||
"label.events": "Events",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "تعديل",
|
||||
"label.edit-dashboard": "تعديل لوحة التحكم",
|
||||
"label.enable-share-url": "تفعيل مشاركة الرابط",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "الأحداث",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Змяніць",
|
||||
"label.edit-dashboard": "Змяніць інфармацыйную панэль",
|
||||
"label.enable-share-url": "Дазволіць дзяліцца спасылкай",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Падзеі",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "সম্পাদনা করুন",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "শেয়ার ইউআরএল শেয়ার করুন",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "ঘটনা",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Edita",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Activa l'enllaç per compartir",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Esdeveniments",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Upravit",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Povolit sdílení URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Události",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Rediger",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Aktivér delings-URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Hændelser",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Bearbeite",
|
||||
"label.edit-dashboard": "Dashboard bearbeite",
|
||||
"label.enable-share-url": "Freigab-URL aktiviere",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Ereigniss",
|
||||
"label.field": "Field",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"label.actions": "Aktionen",
|
||||
"label.activity-log": "Aktivitätsverlauf",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add-description": "Beschreibung hinzufügen",
|
||||
"label.add-website": "Webseite hinzufügen",
|
||||
"label.admin": "Administrator",
|
||||
"label.all": "Alle",
|
||||
@ -42,7 +42,8 @@
|
||||
"label.edit": "Bearbeiten",
|
||||
"label.edit-dashboard": "Dashboard bearbeiten",
|
||||
"label.enable-share-url": "Freigabe-URL aktivieren",
|
||||
"label.event-data": "Event data",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event daten",
|
||||
"label.events": "Ereignisse",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
@ -74,7 +75,7 @@
|
||||
"label.powered-by": "Betrieben durch {name}",
|
||||
"label.profile": "Profil",
|
||||
"label.queries": "Abfragen",
|
||||
"label.query": "Query",
|
||||
"label.query": "Abfrage",
|
||||
"label.query-parameters": "Abfrageparameter",
|
||||
"label.realtime": "Echtzeit",
|
||||
"label.referrers": "Referrer",
|
||||
@ -82,15 +83,15 @@
|
||||
"label.regenerate": "Erneuern",
|
||||
"label.regions": "Regionen",
|
||||
"label.remove": "Entfernen",
|
||||
"label.reports": "Reports",
|
||||
"label.reports": "Reporte",
|
||||
"label.required": "Erforderlich",
|
||||
"label.reset": "Zurücksetzen",
|
||||
"label.reset-website": "Statistik zurücksetzen",
|
||||
"label.role": "Rolle",
|
||||
"label.run-query": "Run query",
|
||||
"label.run-query": "Abfrage starten",
|
||||
"label.save": "Speichern",
|
||||
"label.screens": "Bildschirmauflösungen",
|
||||
"label.select-date": "Select date",
|
||||
"label.select-date": "Datum auswählen",
|
||||
"label.select-website": "Website auswählen",
|
||||
"label.sessions": "Sessions",
|
||||
"label.settings": "Einstellungen",
|
||||
@ -124,31 +125,31 @@
|
||||
"label.view-only": "View only",
|
||||
"label.views": "Aufrufe",
|
||||
"label.visitors": "Besucher",
|
||||
"label.website": "Website",
|
||||
"label.website": "Webseite",
|
||||
"label.website-id": "Webseite ID",
|
||||
"label.websites": "Webseiten",
|
||||
"label.window": "Window",
|
||||
"label.yesterday": "Gestern",
|
||||
"labels.after": "After",
|
||||
"labels.average": "Average",
|
||||
"labels.average": "Durchschnitt",
|
||||
"labels.before": "Before",
|
||||
"labels.breakdown": "Breakdown",
|
||||
"labels.contains": "Contains",
|
||||
"labels.create-report": "Create report",
|
||||
"labels.description": "Description",
|
||||
"labels.create-report": "Report erstellen",
|
||||
"labels.description": "Beschreibung",
|
||||
"labels.does-not-contain": "Does not contain",
|
||||
"labels.does-not-equal": "Does not equal",
|
||||
"labels.equals": "Equals",
|
||||
"labels.false": "False",
|
||||
"labels.filters": "Filters",
|
||||
"labels.greater-than": "Greater than",
|
||||
"labels.greater-than-equals": "Greater than or equals",
|
||||
"labels.less-than": "Less than",
|
||||
"labels.less-than-equals": "Less than or equals",
|
||||
"labels.greater-than": "Größer als",
|
||||
"labels.greater-than-equals": "Größer oder gleich",
|
||||
"labels.less-than": "Kleiner als",
|
||||
"labels.less-than-equals": "Kleiner oder gleich",
|
||||
"labels.max": "Max",
|
||||
"labels.min": "Min",
|
||||
"labels.overview": "Overview",
|
||||
"labels.sum": "Sum",
|
||||
"labels.overview": "Übersicht",
|
||||
"labels.sum": "Summe",
|
||||
"labels.total": "Total",
|
||||
"labels.total-records": "Total records",
|
||||
"labels.true": "True",
|
||||
@ -184,7 +185,7 @@
|
||||
"message.tracking-code": "Tracking Code",
|
||||
"message.user-deleted": "Benutzer gelöscht.",
|
||||
"message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}",
|
||||
"messages.no-results-found": "No results were found.",
|
||||
"messages.no-results-found": "Keine Ergebnisse gefunden.",
|
||||
"messages.no-team-websites": "Diesem Team sind keine Websites zugeordnet.",
|
||||
"messages.no-websites-configured": "Es ist keine Webseite vorhanden.",
|
||||
"messages.team-websites-info": "Webseiten können von jedem im Team eingesehen werden."
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Επεξεργασία",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Γεγονότα",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Edit",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Enable share URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Events",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Edit",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Enable share URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Events",
|
||||
"label.field": "Field",
|
||||
|
192
lang/es-ES.json
Normal file
192
lang/es-ES.json
Normal file
@ -0,0 +1,192 @@
|
||||
{
|
||||
"label.access-code": "Código de acceso",
|
||||
"label.actions": "Acciones",
|
||||
"label.activity-log": "Registro de actividad",
|
||||
"label.add": "Añadir",
|
||||
"label.add-description": "Añadir descripción",
|
||||
"label.add-website": "Nuevo sitio web",
|
||||
"label.admin": "Administrador",
|
||||
"label.all": "Todos",
|
||||
"label.all-time": "Todos los tiempos",
|
||||
"label.analytics": "Analíticas",
|
||||
"label.average-visit-time": "Tiempo promedio de visita",
|
||||
"label.back": "Atrás",
|
||||
"label.bounce-rate": "Porcentaje de rebote",
|
||||
"label.browsers": "Navegadores",
|
||||
"label.cancel": "Cancelar",
|
||||
"label.change-password": "Cambiar contraseña",
|
||||
"label.cities": "Ciudades",
|
||||
"label.clear-all": "Limpiar todo",
|
||||
"label.confirm": "Confirmar",
|
||||
"label.confirm-password": "Confirmar contraseña",
|
||||
"label.continue": "Continuar",
|
||||
"label.countries": "Países",
|
||||
"label.create-team": "Crear equipo",
|
||||
"label.create-user": "Crear usuario",
|
||||
"label.created": "Creado",
|
||||
"label.current-password": "Contraseña actual",
|
||||
"label.custom-range": "Intervalo personalizado",
|
||||
"label.dashboard": "Panel de control",
|
||||
"label.data": "Datos",
|
||||
"label.date-range": "Intervalo de fechas",
|
||||
"label.default-date-range": "Intervalo por defecto",
|
||||
"label.delete": "Eliminar",
|
||||
"label.delete-team": "Eliminar equipo",
|
||||
"label.delete-user": "Eliminar usuario",
|
||||
"label.delete-website": "Eliminar sitio",
|
||||
"label.desktop": "Escritorio",
|
||||
"label.details": "Detalles",
|
||||
"label.devices": "Dispositivos",
|
||||
"label.dismiss": "Ignorar",
|
||||
"label.domain": "Dominio",
|
||||
"label.edit": "Editar",
|
||||
"label.edit-dashboard": "Editar panel",
|
||||
"label.enable-share-url": "Habilitar compartir URL",
|
||||
"label.event": "Evento",
|
||||
"label.event-data": "Datos de evento",
|
||||
"label.events": "Eventos",
|
||||
"label.field": "Campo",
|
||||
"label.fields": "Campos",
|
||||
"label.filter-combined": "Combinado",
|
||||
"label.filter-raw": "En crudo",
|
||||
"label.funnel": "Funnel",
|
||||
"label.join": "Unir",
|
||||
"label.join-team": "Unirse al equipo",
|
||||
"label.language": "Idioma",
|
||||
"label.languages": "Idiomas",
|
||||
"label.laptop": "Portátil",
|
||||
"label.last-days": "Últimos {x} días",
|
||||
"label.last-hours": "Últimas {x} horas",
|
||||
"label.leave": "Abandonar",
|
||||
"label.leave-team": "Abandonar equipo",
|
||||
"label.login": "Iniciar sesión",
|
||||
"label.logout": "Cerrar sesión",
|
||||
"label.members": "Miembros",
|
||||
"label.mobile": "Móvil",
|
||||
"label.more": "Más",
|
||||
"label.name": "Nombre",
|
||||
"label.new-password": "Nueva contraseña",
|
||||
"label.none": "Ninguno",
|
||||
"label.operating-systems": "Sistemas operativos",
|
||||
"label.owner": "Propietario",
|
||||
"label.page-views": "Vistas",
|
||||
"label.pages": "Páginas",
|
||||
"label.password": "Contraseña",
|
||||
"label.powered-by": "Con la ayuda de {name}",
|
||||
"label.profile": "Perfil",
|
||||
"label.queries": "Consultas",
|
||||
"label.query": "Query",
|
||||
"label.query-parameters": "Parámetros de petición",
|
||||
"label.realtime": "Tiempo real",
|
||||
"label.referrers": "Referido desde",
|
||||
"label.refresh": "Actualizar",
|
||||
"label.regenerate": "Regenerar",
|
||||
"label.regions": "Regiones",
|
||||
"label.remove": "Quitar",
|
||||
"label.reports": "Reportes",
|
||||
"label.required": "Obligatorio",
|
||||
"label.reset": "Reiniciar",
|
||||
"label.reset-website": "Reiniciar estadísticas",
|
||||
"label.role": "Rol",
|
||||
"label.run-query": "Ejecutar consulta",
|
||||
"label.save": "Guardar",
|
||||
"label.screens": "Pantallas",
|
||||
"label.select-date": "Seleccionar fecha",
|
||||
"label.select-website": "Seleccionar sitio web",
|
||||
"label.sessions": "Sesiones",
|
||||
"label.settings": "Configuraciones",
|
||||
"label.share-url": "Compartir URL",
|
||||
"label.single-day": "Un solo día",
|
||||
"label.tablet": "Tableta",
|
||||
"label.team": "Equipo",
|
||||
"label.team-guest": "Invitado al equipo",
|
||||
"label.team-id": "ID de equipo",
|
||||
"label.team-member": "Miembro del equipo",
|
||||
"label.team-owner": "Admin. del equipo",
|
||||
"label.teams": "Equipos",
|
||||
"label.theme": "Tema",
|
||||
"label.this-month": "Este mes",
|
||||
"label.this-week": "Esta semana",
|
||||
"label.this-year": "Este año",
|
||||
"label.timezone": "Zona horaria",
|
||||
"label.title": "Título",
|
||||
"label.today": "Hoy",
|
||||
"label.toggle-charts": "Alternar gráficas",
|
||||
"label.tracking-code": "Código de rastreo",
|
||||
"label.unique-visitors": "Visitantes únicos",
|
||||
"label.unknown": "Desconocida",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.user": "Usuario",
|
||||
"label.username": "Nombre de usuario",
|
||||
"label.users": "Usuarios",
|
||||
"label.view": "Visualizar",
|
||||
"label.view-details": "Ver detalles",
|
||||
"label.view-only": "Ver sólo",
|
||||
"label.views": "Vistas",
|
||||
"label.visitors": "Visitantes",
|
||||
"label.website": "Sitio web",
|
||||
"label.website-id": "ID del sitio web",
|
||||
"label.websites": "Sitios web",
|
||||
"label.window": "Ventana",
|
||||
"label.yesterday": "Ayer",
|
||||
"labels.after": "Después",
|
||||
"labels.average": "Media",
|
||||
"labels.before": "Antes",
|
||||
"labels.breakdown": "Desglose",
|
||||
"labels.contains": "Contiene",
|
||||
"labels.create-report": "Crear reporte",
|
||||
"labels.description": "Descripciones",
|
||||
"labels.does-not-contain": "No contiene",
|
||||
"labels.does-not-equal": "No es igual a",
|
||||
"labels.equals": "Es igual a",
|
||||
"labels.false": "False",
|
||||
"labels.filters": "Filtros",
|
||||
"labels.greater-than": "Mayor que",
|
||||
"labels.greater-than-equals": "Mayor que o igual a",
|
||||
"labels.less-than": "Menor que",
|
||||
"labels.less-than-equals": "Menor que o igual a",
|
||||
"labels.max": "Máx",
|
||||
"labels.min": "Mín",
|
||||
"labels.overview": "Resumen",
|
||||
"labels.sum": "Suma",
|
||||
"labels.total": "Total",
|
||||
"labels.total-records": "Total de registros",
|
||||
"labels.true": "Verdadero",
|
||||
"labels.type": "Tipo",
|
||||
"labels.unique": "Único",
|
||||
"labels.untitled": "Sin título",
|
||||
"labels.value": "Valor",
|
||||
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
|
||||
"message.confirm-delete": "¿Seguro que quieres eliminar {target}?",
|
||||
"message.confirm-leave": "¿Seguro que quieres abandonar {target}?",
|
||||
"message.confirm-reset": "¿Seguro que quieres BORRAR las analíticas de {target}?",
|
||||
"message.delete-account": "Para borrar esta cuenta, escribe {confirmation} a continuación para confirmar.",
|
||||
"message.delete-website": "Para borrar este sitio web, escribe {confirmation} a continuación para confirmar.",
|
||||
"message.delete-website-warning": "Toda la información relacionada será eliminada.",
|
||||
"message.error": "Algo falló.",
|
||||
"message.event-log": "{event} en {url}",
|
||||
"message.go-to-settings": "Ir a la configuración",
|
||||
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
|
||||
"message.invalid-domain": "Dominio inválido",
|
||||
"message.min-password-length": "Longitud mínima de {n} caracteres",
|
||||
"message.no-data-available": "No hay información disponible.",
|
||||
"message.no-event-data": "No hay datos de eventos disponibles.",
|
||||
"message.no-match-password": "Las contraseñas no coinciden",
|
||||
"message.no-teams": "No has creado ningún equipo.",
|
||||
"message.no-users": "No hay usuarios.",
|
||||
"message.page-not-found": "Página no encontrada",
|
||||
"message.reset-website": "Para reiniciar este sitio web, escribe {confirmation} a continuación para confirmar.",
|
||||
"message.reset-website-warning": "Todas las estadísticas de esta página serán eliminadas, pero el código de rastreo permanecerá intacto.",
|
||||
"message.saved": "Guardado.",
|
||||
"message.share-url": "Esta es la URL pública para {target}.",
|
||||
"message.team-already-member": "Ya eres miembro de este equipo.",
|
||||
"message.team-not-found": "Equipo no encontrado.",
|
||||
"message.tracking-code": "Código de rastreo",
|
||||
"message.user-deleted": "Usuario eliminado.",
|
||||
"message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}",
|
||||
"messages.no-results-found": "No se encontraron resultados.",
|
||||
"messages.no-team-websites": "Este equipo no tiene ningún sitio web configurado.",
|
||||
"messages.no-websites-configured": "No tienes ningún sitio web configurado.",
|
||||
"messages.team-websites-info": "Las analíticas de tus sitios web pueden ser vistas por cualquier miembro del equipo."
|
||||
}
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Editar",
|
||||
"label.edit-dashboard": "Editar panel",
|
||||
"label.enable-share-url": "Habilitar compartir URL",
|
||||
"label.event": "Evento",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Eventos",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "ویرایش",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "فعال کردن اشتراک گذاری URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "رویدادها",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Muokkaa",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Ota jakamisen URL-osoite käyttöön",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Tapahtumat",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Ger broyting",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Virkja deili leinki",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Hendingar/tiltøk",
|
||||
"label.field": "Field",
|
||||
|
@ -2,8 +2,8 @@
|
||||
"label.access-code": "Code d'accès",
|
||||
"label.actions": "Actions",
|
||||
"label.activity-log": "Journal d'activité",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add": "Ajouter",
|
||||
"label.add-description": "Ajouter une description",
|
||||
"label.add-website": "Ajouter un site",
|
||||
"label.admin": "Administrateur",
|
||||
"label.all": "Tout",
|
||||
@ -42,13 +42,14 @@
|
||||
"label.edit": "Modifier",
|
||||
"label.edit-dashboard": "Modifier le tableau de bord",
|
||||
"label.enable-share-url": "Activer l'URL de partage",
|
||||
"label.event-data": "Event data",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Données d'événements",
|
||||
"label.events": "Événements",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
"label.field": "Champ",
|
||||
"label.fields": "Champs",
|
||||
"label.filter-combined": "Combiné",
|
||||
"label.filter-raw": "Brut",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel": "Entonnoir",
|
||||
"label.join": "Rejoindre",
|
||||
"label.join-team": "Rejoindre une équipe",
|
||||
"label.language": "Langue",
|
||||
@ -73,24 +74,24 @@
|
||||
"label.password": "Mot de passe",
|
||||
"label.powered-by": "Propulsé par {name}",
|
||||
"label.profile": "Profil",
|
||||
"label.queries": "Queries",
|
||||
"label.query": "Query",
|
||||
"label.queries": "Requêtes",
|
||||
"label.query": "Requête",
|
||||
"label.query-parameters": "Paramètres d'URL",
|
||||
"label.realtime": "Temps réel",
|
||||
"label.referrers": "Sources",
|
||||
"label.referrers": "Sites référents",
|
||||
"label.refresh": "Rafraîchir",
|
||||
"label.regenerate": "Régénérer",
|
||||
"label.regions": "Régions",
|
||||
"label.remove": "Retirer",
|
||||
"label.reports": "Reports",
|
||||
"label.reports": "Rapports",
|
||||
"label.required": "Requis",
|
||||
"label.reset": "Réinitialiser",
|
||||
"label.reset-website": "Réinitialiser les statistiques",
|
||||
"label.role": "Rôle",
|
||||
"label.run-query": "Run query",
|
||||
"label.run-query": "Éxécuter la requête",
|
||||
"label.save": "Enregistrer",
|
||||
"label.screens": "Résolutions d'écran",
|
||||
"label.select-date": "Select date",
|
||||
"label.select-date": "Choisir une période",
|
||||
"label.select-website": "Choisir un site",
|
||||
"label.sessions": "Sessions",
|
||||
"label.settings": "Paramètres",
|
||||
@ -121,46 +122,46 @@
|
||||
"label.users": "Utilisateurs",
|
||||
"label.view": "Voir",
|
||||
"label.view-details": "Voir les détails",
|
||||
"label.view-only": "View only",
|
||||
"label.view-only": "Consultation",
|
||||
"label.views": "Vues",
|
||||
"label.visitors": "Visiteurs",
|
||||
"label.website": "Website",
|
||||
"label.website-id": "ID de site",
|
||||
"label.websites": "Sites",
|
||||
"label.window": "Window",
|
||||
"label.window": "Fenêtre",
|
||||
"label.yesterday": "Hier",
|
||||
"labels.after": "After",
|
||||
"labels.average": "Average",
|
||||
"labels.before": "Before",
|
||||
"labels.breakdown": "Breakdown",
|
||||
"labels.contains": "Contains",
|
||||
"labels.create-report": "Create report",
|
||||
"labels.after": "Après",
|
||||
"labels.average": "Moyenne",
|
||||
"labels.before": "Avant",
|
||||
"labels.breakdown": "Répartition",
|
||||
"labels.contains": "Contient",
|
||||
"labels.create-report": "Créer un rapport",
|
||||
"labels.description": "Description",
|
||||
"labels.does-not-contain": "Does not contain",
|
||||
"labels.does-not-equal": "Does not equal",
|
||||
"labels.equals": "Equals",
|
||||
"labels.false": "False",
|
||||
"labels.filters": "Filters",
|
||||
"labels.greater-than": "Greater than",
|
||||
"labels.greater-than-equals": "Greater than or equals",
|
||||
"labels.less-than": "Less than",
|
||||
"labels.less-than-equals": "Less than or equals",
|
||||
"labels.does-not-contain": "Ne contient pas",
|
||||
"labels.does-not-equal": "N'est pas égal",
|
||||
"labels.equals": "Est égal",
|
||||
"labels.false": "Faux",
|
||||
"labels.filters": "Filtres",
|
||||
"labels.greater-than": "Supérieur à",
|
||||
"labels.greater-than-equals": "Supérieur ou égal à",
|
||||
"labels.less-than": "Inférieur à",
|
||||
"labels.less-than-equals": "Inférieur ou égal à",
|
||||
"labels.max": "Max",
|
||||
"labels.min": "Min",
|
||||
"labels.overview": "Overview",
|
||||
"labels.sum": "Sum",
|
||||
"labels.overview": "Vue d'ensemble",
|
||||
"labels.sum": "Somme",
|
||||
"labels.total": "Total",
|
||||
"labels.total-records": "Total records",
|
||||
"labels.true": "True",
|
||||
"labels.total-records": "Nombre d'enregistrements",
|
||||
"labels.true": "Vrai",
|
||||
"labels.type": "Type",
|
||||
"labels.unique": "Unique",
|
||||
"labels.untitled": "Untitled",
|
||||
"labels.value": "Value",
|
||||
"labels.untitled": "Sans titre",
|
||||
"labels.value": "Valeur",
|
||||
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
|
||||
"message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?",
|
||||
"message.confirm-leave": "Êtes-vous sûr de vouloir quitter {target} ?",
|
||||
"message.confirm-reset": "Êtes-vous sûr de vouloir réinitialiser les statistiques de {target} ?",
|
||||
"message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.",
|
||||
"message.delete-account": "Pour supprimer ce compte, taper {confirmation} ci-dessous pour confirmer.",
|
||||
"message.delete-website": "Pour supprimer ce site, taper {confirmation} ci-dessous pour confirmer.",
|
||||
"message.delete-website-warning": "Toutes les données associées seront supprimées.",
|
||||
"message.error": "Un problème est survenu.",
|
||||
@ -170,12 +171,12 @@
|
||||
"message.invalid-domain": "Domaine invalide",
|
||||
"message.min-password-length": "Taille minimale de {n} caractères",
|
||||
"message.no-data-available": "Aucune donnée disponible.",
|
||||
"message.no-event-data": "No event data is available.",
|
||||
"message.no-event-data": "Aucune donnée d'événement disponible.",
|
||||
"message.no-match-password": "Les mots de passe ne correspondent pas",
|
||||
"message.no-teams": "Vous n'avez créé aucune équipe.",
|
||||
"message.no-users": "Il n'y aucun utilisateur.",
|
||||
"message.no-teams": "Vous n'avez pas créé d'équipe.",
|
||||
"message.no-users": "Aucun utilisateur.",
|
||||
"message.page-not-found": "Page non trouvée.",
|
||||
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
|
||||
"message.reset-website": "Pour réinitialiser ce site, taper {confirmation} ci-dessous pour confirmer.",
|
||||
"message.reset-website-warning": "Toutes les statistiques pour ce site seront supprimées, mais votre code de suivi restera intact.",
|
||||
"message.saved": "Enregistré avec succès.",
|
||||
"message.share-url": "Les statistiques de votre site sont accessibles publiquement sur cette URL :",
|
||||
@ -184,8 +185,8 @@
|
||||
"message.tracking-code": "Code de suivi",
|
||||
"message.user-deleted": "Utilisateur supprimé.",
|
||||
"message.visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}",
|
||||
"messages.no-results-found": "No results were found.",
|
||||
"messages.no-results-found": "Aucun résultat n'a été trouvé.",
|
||||
"messages.no-team-websites": "Cette équipe n'a aucun site.",
|
||||
"messages.no-websites-configured": "Vous n'avez configuré aucun site.",
|
||||
"messages.no-websites-configured": "Vous n'avez pas configuré de site.",
|
||||
"messages.team-websites-info": "Les sites peuvent être vus par tout utilisateur dans l'équipe."
|
||||
}
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Editar",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Activar URL de compartición",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Eventos",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "עריכה",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "הפעלת URL שיתוף",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "אירועים",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "संपादित करें",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "शेयर URL सक्षम करें",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "स्पर्धाएँ",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Uredi",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Omogući dijeljenje poveznice",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Podaci događaja",
|
||||
"label.events": "Events",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Módosítás",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "URL-megosztás engedélyezése",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Események",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Sunting",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Aktifkan URL berbagi",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Perihal",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Modifica",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Abilita URL di condivisione",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Eventi",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "編集",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "共有リンクを有効にする",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "イベント",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "កែប្រែ",
|
||||
"label.edit-dashboard": "កែផ្ទាំងគ្រប់គ្រង",
|
||||
"label.enable-share-url": "បើកការចែករំលែក URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "ព្រឹត្តិការណ៍",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "편집",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "URL 공유 활성화",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "이벤트",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Redaguoti",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Įjungti bendrinimą su nuoroda",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Įvykiai",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Засах",
|
||||
"label.edit-dashboard": "Хянах самбар засах",
|
||||
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Үйлдэл",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Edit",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Aktifkan url berkongsi",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Peristiwa",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Rediger",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Aktiver delings-URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Arrangementer",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Bewerken",
|
||||
"label.edit-dashboard": "Dashboard aanpassen",
|
||||
"label.enable-share-url": "Sta delen via openbare URL toe",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Gebeurtenissen",
|
||||
"label.field": "Field",
|
||||
|
@ -2,8 +2,8 @@
|
||||
"label.access-code": "Kod dostępu",
|
||||
"label.actions": "Działania",
|
||||
"label.activity-log": "Dziennik aktywności",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add": "Dodaj",
|
||||
"label.add-description": "Dodaj opis",
|
||||
"label.add-website": "Dodaj witrynę",
|
||||
"label.admin": "Administrator",
|
||||
"label.all": "Wszystkie",
|
||||
@ -42,13 +42,14 @@
|
||||
"label.edit": "Edytuj",
|
||||
"label.edit-dashboard": "Edytuj panel",
|
||||
"label.enable-share-url": "Włącz udostępnianie adresu URL",
|
||||
"label.event-data": "Event data",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Dane zdarzenia",
|
||||
"label.events": "Zdarzenia",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
"label.field": "Pole",
|
||||
"label.fields": "Pola",
|
||||
"label.filter-combined": "Połączone",
|
||||
"label.filter-raw": "Surowe dane",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel": "Lejek",
|
||||
"label.join": "Dołącz",
|
||||
"label.join-team": "Dołącz do zespołu",
|
||||
"label.language": "Język",
|
||||
@ -74,23 +75,23 @@
|
||||
"label.powered-by": "Obsługiwane przez {name}",
|
||||
"label.profile": "Profil",
|
||||
"label.queries": "Zapytania",
|
||||
"label.query": "Query",
|
||||
"label.query-parameters": "Parametry query",
|
||||
"label.query": "Zapytanie",
|
||||
"label.query-parameters": "Parametry zapytania",
|
||||
"label.realtime": "Czas rzeczywisty",
|
||||
"label.referrers": "Źródła odsyłające",
|
||||
"label.refresh": "Odśwież",
|
||||
"label.regenerate": "Wygeneruj ponownie",
|
||||
"label.regions": "Regiony",
|
||||
"label.remove": "Usuń",
|
||||
"label.reports": "Reports",
|
||||
"label.reports": "Raporty",
|
||||
"label.required": "Wymagany",
|
||||
"label.reset": "Zresetuj",
|
||||
"label.reset-website": "Zresetuj statystyki",
|
||||
"label.role": "Role",
|
||||
"label.run-query": "Run query",
|
||||
"label.run-query": "Uruchom zapytanie",
|
||||
"label.save": "Zapisz",
|
||||
"label.screens": "Ekrany",
|
||||
"label.select-date": "Select date",
|
||||
"label.select-date": "Wybierz datę",
|
||||
"label.select-website": "Wybierz witrynę",
|
||||
"label.sessions": "Sesje",
|
||||
"label.settings": "Ustawienia",
|
||||
@ -114,54 +115,54 @@
|
||||
"label.tracking-code": "Kod śledzenia",
|
||||
"label.unique-visitors": "Unikalni odwiedzający",
|
||||
"label.unknown": "Nieznany",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.url": "Link",
|
||||
"label.urls": "Linki",
|
||||
"label.user": "Użytkownik",
|
||||
"label.username": "Nazwa użytkownika",
|
||||
"label.users": "Użytkownicy",
|
||||
"label.view": "Zobacz",
|
||||
"label.view-details": "Pokaż szczegóły",
|
||||
"label.view-only": "View only",
|
||||
"label.view-only": "Tylko do odczytu",
|
||||
"label.views": "Wyświetlenia",
|
||||
"label.visitors": "Odwiedzający",
|
||||
"label.website": "Website",
|
||||
"label.website": "Witryna",
|
||||
"label.website-id": "ID witryny",
|
||||
"label.websites": "Witryny",
|
||||
"label.window": "Window",
|
||||
"label.window": "Okno",
|
||||
"label.yesterday": "Wczoraj",
|
||||
"labels.after": "After",
|
||||
"labels.average": "Average",
|
||||
"labels.before": "Before",
|
||||
"labels.breakdown": "Breakdown",
|
||||
"labels.contains": "Contains",
|
||||
"labels.create-report": "Create report",
|
||||
"labels.description": "Description",
|
||||
"labels.does-not-contain": "Does not contain",
|
||||
"labels.does-not-equal": "Does not equal",
|
||||
"labels.equals": "Equals",
|
||||
"labels.false": "False",
|
||||
"labels.filters": "Filters",
|
||||
"labels.greater-than": "Greater than",
|
||||
"labels.greater-than-equals": "Greater than or equals",
|
||||
"labels.less-than": "Less than",
|
||||
"labels.less-than-equals": "Less than or equals",
|
||||
"labels.max": "Max",
|
||||
"labels.after": "Po",
|
||||
"labels.average": "Średnia",
|
||||
"labels.before": "Przed",
|
||||
"labels.breakdown": "Rozbicie",
|
||||
"labels.contains": "Zawiera",
|
||||
"labels.create-report": "Stwórz raport",
|
||||
"labels.description": "Opis",
|
||||
"labels.does-not-contain": "Nie zawiera",
|
||||
"labels.does-not-equal": "Nie jest równe",
|
||||
"labels.equals": "Równe",
|
||||
"labels.false": "Fałsz",
|
||||
"labels.filters": "Filtry",
|
||||
"labels.greater-than": "Większe niż",
|
||||
"labels.greater-than-equals": "Większe niż lub równe",
|
||||
"labels.less-than": "Mniejsze niż",
|
||||
"labels.less-than-equals": "Mniejsze niż lub równe",
|
||||
"labels.max": "Maks",
|
||||
"labels.min": "Min",
|
||||
"labels.overview": "Overview",
|
||||
"labels.sum": "Sum",
|
||||
"labels.total": "Total",
|
||||
"labels.total-records": "Total records",
|
||||
"labels.true": "True",
|
||||
"labels.type": "Type",
|
||||
"labels.unique": "Unique",
|
||||
"labels.untitled": "Untitled",
|
||||
"labels.value": "Value",
|
||||
"message.active-users": "{x} aktualnie {x, plural, one {odwiedzający} other {odwiedzających}}",
|
||||
"labels.overview": "Przegląd",
|
||||
"labels.sum": "Suma",
|
||||
"labels.total": "W sumie",
|
||||
"labels.total-records": "Suma rekordów",
|
||||
"labels.true": "Prawda",
|
||||
"labels.type": "Typ",
|
||||
"labels.unique": "Unikalne",
|
||||
"labels.untitled": "Bez tytułu",
|
||||
"labels.value": "Wartość",
|
||||
"message.active-users": "{x} aktualnie {x, liczba mnoga, jeden {odwiedzający} inne {odwiedzających}}",
|
||||
"message.confirm-delete": "Czy na pewno chcesz usunąć {target}?",
|
||||
"message.confirm-leave": "Czy na pewno chcesz opuścić {target}?",
|
||||
"message.confirm-reset": "Czy na pewno chcesz zresetować statystyki {target}?",
|
||||
"message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.",
|
||||
"message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.",
|
||||
"message.delete-account": "Aby usunąć to konto, wpisz {confirmation} w polu poniżej, aby potwierdzić.",
|
||||
"message.delete-website": "Aby usunąć tę stronę, wpisz {confirmation} w polu poniżej, aby potwierdzić.",
|
||||
"message.delete-website-warning": "Wszystkie powiązane dane również zostaną usunięte.",
|
||||
"message.error": "Coś poszło nie tak.",
|
||||
"message.event-log": "{event} na {url}",
|
||||
@ -170,7 +171,7 @@
|
||||
"message.invalid-domain": "Nieprawidłowa witryna",
|
||||
"message.min-password-length": "Minimalna długość {n} znaków",
|
||||
"message.no-data-available": "Brak dostępnych danych.",
|
||||
"message.no-event-data": "No event data is available.",
|
||||
"message.no-event-data": "Brak dostępnych danych o zdarzeniach.",
|
||||
"message.no-match-password": "Hasła się nie zgadzają",
|
||||
"message.no-teams": "Nie stworzyłeś żadnych zespołów.",
|
||||
"message.no-users": "Nie ma żadnych użytkowników.",
|
||||
@ -184,7 +185,7 @@
|
||||
"message.tracking-code": "Kod śledzenia",
|
||||
"message.user-deleted": "Użytkownik usunięty.",
|
||||
"message.visitor-log": "Odwiedzający z {country} używa {browser} na {os} {device}",
|
||||
"messages.no-results-found": "No results were found.",
|
||||
"messages.no-results-found": "Nie znaleziono wyników.",
|
||||
"messages.no-team-websites": "Ten zespół nie ma żadnych witryn internetowych.",
|
||||
"messages.no-websites-configured": "Nie masz skonfigurowanych żadnych witryn internetowych.",
|
||||
"messages.team-websites-info": "Strony internetowe mogą być przeglądane przez każdego członka zespołu."
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Editar",
|
||||
"label.edit-dashboard": "Editar painel",
|
||||
"label.enable-share-url": "Ativar link de compartilhamento",
|
||||
"label.event": "Evento",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Eventos",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Editar",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Ativar link de partilha",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Eventos",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Editare",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Activare adresă URL de distribuire",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Evenimente",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Изменить",
|
||||
"label.edit-dashboard": "Редактировать дашборд",
|
||||
"label.enable-share-url": "Разрешить делиться ссылкой",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "События",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "සංස්කරණය කරන්න",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "බෙදාගැනීමේ URL සබල කරන්න",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "සිදුවීම් දත්ත",
|
||||
"label.events": "Events",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Upraviť",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Povoliť zdielanie URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Udalosti",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Uredi",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Omogoči URL za skupno rabo",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Dogodki",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Redigera",
|
||||
"label.edit-dashboard": "Redigera översikt",
|
||||
"label.enable-share-url": "Aktivera delnings-URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Händelser",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "திருத்துதல்",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "கள முகவரியை பகிரலாம்",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "நிகழ்வுகள்",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "แก้ไข",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "เปิดใช้งานการแชร์ลิงก์",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "เหตุการณ์",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Düzenle",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Olaylar",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Редагувати",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Дозволити ділитися посиланням",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Події",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "ترمیم",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "شیئر یو آر ایل کو فعال کریں",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "واقعات",
|
||||
"label.field": "Field",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "Chỉnh sửa",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.enable-share-url": "Bật khả năng chia sẻ URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Sự kiện",
|
||||
"label.field": "Field",
|
||||
|
@ -2,8 +2,8 @@
|
||||
"label.access-code": "访问代码",
|
||||
"label.actions": "用户行为",
|
||||
"label.activity-log": "活动日志",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add": "添加",
|
||||
"label.add-description": "添加描述",
|
||||
"label.add-website": "添加网站",
|
||||
"label.admin": "管理员",
|
||||
"label.all": "所有",
|
||||
@ -42,6 +42,7 @@
|
||||
"label.edit": "编辑",
|
||||
"label.edit-dashboard": "编辑仪表板",
|
||||
"label.enable-share-url": "启用共享链接",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "行为类别",
|
||||
"label.field": "Field",
|
||||
@ -127,15 +128,15 @@
|
||||
"label.website": "Website",
|
||||
"label.website-id": "网站 ID",
|
||||
"label.websites": "网站",
|
||||
"label.window": "Window",
|
||||
"label.window": "窗口",
|
||||
"label.yesterday": "昨天",
|
||||
"labels.after": "After",
|
||||
"labels.average": "Average",
|
||||
"labels.before": "Before",
|
||||
"labels.breakdown": "Breakdown",
|
||||
"labels.contains": "Contains",
|
||||
"labels.create-report": "Create report",
|
||||
"labels.description": "Description",
|
||||
"labels.create-report": "创建报告",
|
||||
"labels.description": "描述",
|
||||
"labels.does-not-contain": "Does not contain",
|
||||
"labels.does-not-equal": "Does not equal",
|
||||
"labels.equals": "Equals",
|
||||
@ -154,14 +155,14 @@
|
||||
"labels.true": "True",
|
||||
"labels.type": "Type",
|
||||
"labels.unique": "Unique",
|
||||
"labels.untitled": "Untitled",
|
||||
"labels.untitled": "未命名",
|
||||
"labels.value": "Value",
|
||||
"message.active-users": "当前在线 {x} 人",
|
||||
"message.confirm-delete": "你确定要删除 {target} 吗?",
|
||||
"message.confirm-leave": "你确定要离开 {target} 吗?",
|
||||
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
|
||||
"message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.",
|
||||
"message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.",
|
||||
"message.delete-account": "确定删除该账户, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
|
||||
"message.delete-website": "确定删除该网站, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
|
||||
"message.delete-website-warning": "所有相关数据将会被删除。",
|
||||
"message.error": "出现错误。",
|
||||
"message.event-log": "{event} on {url}",
|
||||
@ -175,7 +176,7 @@
|
||||
"message.no-teams": "你还没有创建任何团队。",
|
||||
"message.no-users": "没有任何用户。",
|
||||
"message.page-not-found": "网页未找到。",
|
||||
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
|
||||
"message.reset-website": "确定重置该网站, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
|
||||
"message.reset-website-warning": "本网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
|
||||
"message.saved": "保存成功。",
|
||||
"message.share-url": "这是 {target} 的共享链接。",
|
||||
@ -184,7 +185,7 @@
|
||||
"message.tracking-code": "跟踪代码",
|
||||
"message.user-deleted": "User detected.",
|
||||
"message.visitor-log": "来自{country}的访客在搭载 {os} 的{device}上使用 {browser} 浏览器进行访问。",
|
||||
"messages.no-results-found": "No results were found.",
|
||||
"messages.no-results-found": "没有找到任何结果。",
|
||||
"messages.no-team-websites": "这个团队没有任何网站。",
|
||||
"messages.no-websites-configured": "你还没有设置任何网站。",
|
||||
"messages.team-websites-info": "团队中的任何人都可查看网站。"
|
||||
|
@ -42,6 +42,7 @@
|
||||
"label.edit": "編輯",
|
||||
"label.edit-dashboard": "編輯管理面板",
|
||||
"label.enable-share-url": "啟用分享連結",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "行為類別",
|
||||
"label.field": "Field",
|
||||
|
15
lib/auth.ts
15
lib/auth.ts
@ -2,7 +2,7 @@ import { Report } from '@prisma/client';
|
||||
import redis from '@umami/redis-client';
|
||||
import debug from 'debug';
|
||||
import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||
import { secret } from 'lib/crypto';
|
||||
import { secret, isUuid } from 'lib/crypto';
|
||||
import {
|
||||
createSecureToken,
|
||||
ensureArray,
|
||||
@ -12,8 +12,7 @@ import {
|
||||
} from 'next-basics';
|
||||
import { getTeamUser } from 'queries';
|
||||
import { getTeamWebsite, getTeamWebsiteByTeamMemberId } from 'queries/admin/teamWebsite';
|
||||
import { validate } from 'uuid';
|
||||
import { loadWebsite } from './query';
|
||||
import { loadWebsite } from './load';
|
||||
import { Auth } from './types';
|
||||
|
||||
const log = debug('umami:auth');
|
||||
@ -108,7 +107,7 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!validate(websiteId)) {
|
||||
if (!isUuid(websiteId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -184,7 +183,7 @@ export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (validate(teamId)) {
|
||||
if (isUuid(teamId)) {
|
||||
const teamUser = await getTeamUser(teamId, user.id);
|
||||
|
||||
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
|
||||
@ -198,7 +197,7 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (validate(teamId)) {
|
||||
if (isUuid(teamId)) {
|
||||
const teamUser = await getTeamUser(teamId, user.id);
|
||||
|
||||
return hasPermission(teamUser.role, PERMISSIONS.teamDelete);
|
||||
@ -212,7 +211,7 @@ export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUs
|
||||
return true;
|
||||
}
|
||||
|
||||
if (validate(teamId) && validate(removeUserId)) {
|
||||
if (isUuid(teamId) && isUuid(removeUserId)) {
|
||||
if (removeUserId === user.id) {
|
||||
return true;
|
||||
}
|
||||
@ -230,7 +229,7 @@ export async function canDeleteTeamWebsite({ user }: Auth, teamId: string, websi
|
||||
return true;
|
||||
}
|
||||
|
||||
if (validate(teamId) && validate(websiteId)) {
|
||||
if (isUuid(teamId) && isUuid(websiteId)) {
|
||||
const teamWebsite = await getTeamWebsite(teamId, websiteId);
|
||||
|
||||
if (teamWebsite.website.userId === user.id) {
|
||||
|
@ -2,7 +2,6 @@ import { ClickHouse } from 'clickhouse';
|
||||
import dateFormat from 'dateformat';
|
||||
import debug from 'debug';
|
||||
import { CLICKHOUSE } from 'lib/db';
|
||||
import { getDynamicDataType } from './dynamicData';
|
||||
import { WebsiteMetricFilter } from './types';
|
||||
import { FILTER_COLUMNS } from './constants';
|
||||
|
||||
@ -62,49 +61,6 @@ function getDateFormat(date) {
|
||||
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
||||
}
|
||||
|
||||
function getBetweenDates(field, startAt, endAt) {
|
||||
return `${field} between ${getDateFormat(startAt)} and ${getDateFormat(endAt)}`;
|
||||
}
|
||||
|
||||
function getEventDataFilterQuery(
|
||||
filters: {
|
||||
eventKey?: string;
|
||||
eventValue?: string | number | boolean | Date;
|
||||
}[] = [],
|
||||
params: any,
|
||||
) {
|
||||
const query = filters.reduce((ac, cv, i) => {
|
||||
const type = getDynamicDataType(cv.eventValue);
|
||||
|
||||
let value = cv.eventValue;
|
||||
|
||||
ac.push(`and (event_key = {eventKey${i}:String}`);
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
ac.push(`and number_value = {eventValue${i}:UInt64})`);
|
||||
break;
|
||||
case 'string':
|
||||
ac.push(`and string_value = {eventValue${i}:String})`);
|
||||
break;
|
||||
case 'boolean':
|
||||
ac.push(`and string_value = {eventValue${i}:String})`);
|
||||
value = cv ? 'true' : 'false';
|
||||
break;
|
||||
case 'date':
|
||||
ac.push(`and date_value = {eventValue${i}:DateTime('UTC')})`);
|
||||
break;
|
||||
}
|
||||
|
||||
params[`eventKey${i}`] = cv.eventKey;
|
||||
params[`eventValue${i}`] = value;
|
||||
|
||||
return ac;
|
||||
}, []);
|
||||
|
||||
return query.join('\n');
|
||||
}
|
||||
|
||||
function getFilterQuery(filters = {}, params = {}) {
|
||||
const query = Object.keys(filters).reduce((arr, key) => {
|
||||
const filter = filters[key];
|
||||
@ -121,36 +77,13 @@ function getFilterQuery(filters = {}, params = {}) {
|
||||
return query.join('\n');
|
||||
}
|
||||
|
||||
function getFunnelQuery(urls: string[]): {
|
||||
columnsQuery: string;
|
||||
conditionQuery: string;
|
||||
urlParams: { [key: string]: string };
|
||||
} {
|
||||
return urls.reduce(
|
||||
(pv, cv, i) => {
|
||||
pv.columnsQuery += `\n,url_path = {url${i}:String}${
|
||||
i > 0 && urls[i - 1] ? ` AND referrer_path = {url${i - 1}:String}` : ''
|
||||
}`;
|
||||
pv.conditionQuery += `${i > 0 ? ',' : ''} {url${i}:String}`;
|
||||
pv.urlParams[`url${i}`] = cv;
|
||||
|
||||
return pv;
|
||||
},
|
||||
{
|
||||
columnsQuery: '',
|
||||
conditionQuery: '',
|
||||
urlParams: {},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function parseFilters(filters: WebsiteMetricFilter = {}, params: any = {}) {
|
||||
return {
|
||||
filterQuery: getFilterQuery(filters, params),
|
||||
};
|
||||
}
|
||||
|
||||
async function rawQuery<T>(query, params = {}): Promise<T> {
|
||||
async function rawQuery<T>(query: string, params: object = {}): Promise<T> {
|
||||
if (process.env.LOG_QUERY) {
|
||||
log('QUERY:\n', query);
|
||||
log('PARAMETERS:\n', params);
|
||||
@ -166,7 +99,7 @@ async function findUnique(data) {
|
||||
throw `${data.length} records found when expecting 1.`;
|
||||
}
|
||||
|
||||
return data[0] ?? null;
|
||||
return findFirst(data);
|
||||
}
|
||||
|
||||
async function findFirst(data) {
|
||||
@ -189,10 +122,7 @@ export default {
|
||||
getDateStringQuery,
|
||||
getDateQuery,
|
||||
getDateFormat,
|
||||
getBetweenDates,
|
||||
getFilterQuery,
|
||||
getFunnelQuery,
|
||||
getEventDataFilterQuery,
|
||||
parseFilters,
|
||||
findUnique,
|
||||
findFirst,
|
||||
|
@ -13,12 +13,12 @@ export const REPO_URL = 'https://github.com/umami-software/umami';
|
||||
export const UPDATES_URL = 'https://api.umami.is/v1/updates';
|
||||
export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png';
|
||||
|
||||
export const DEFAULT_LOCALE = 'en-US';
|
||||
export const DEFAULT_LOCALE = process.env.defaultLocale ?? 'en-US';
|
||||
export const DEFAULT_THEME = 'light';
|
||||
export const DEFAULT_ANIMATION_DURATION = 300;
|
||||
export const DEFAULT_DATE_RANGE = '24hour';
|
||||
export const DEFAULT_WEBSITE_LIMIT = 10;
|
||||
export const DEFAULT_CREATED_AT = '2000-01-01';
|
||||
export const DEFAULT_RESET_DATE = '2000-01-01';
|
||||
|
||||
export const REALTIME_RANGE = 30;
|
||||
export const REALTIME_INTERVAL = 5000;
|
||||
@ -120,6 +120,37 @@ export const ROLE_PERMISSIONS = {
|
||||
[ROLES.teamMember]: [],
|
||||
} as const;
|
||||
|
||||
export const WEBSITE_EVENT_FIELDS = {
|
||||
eventId: { name: 'event_id', type: 'uuid', label: 'Event ID' },
|
||||
websiteId: { name: 'website_id', type: 'uuid', label: 'Website ID' },
|
||||
sessionId: { name: 'session_id', type: 'uuid', label: 'Session ID' },
|
||||
createdAt: { name: 'created_at', type: 'date', label: 'Created date' },
|
||||
urlPath: { name: 'url_path', type: 'string', label: 'URL path' },
|
||||
urlQuery: { name: 'url_query', type: 'string', label: 'URL query' },
|
||||
referrerPath: { name: 'referrer_path', type: 'string', label: 'Referrer path' },
|
||||
referrerQuery: { name: 'referrer_query', type: 'string', label: 'Referrer query' },
|
||||
referrerDomain: { name: 'referrer_domain', type: 'string', label: 'Referrer domain' },
|
||||
pageTitle: { name: 'page_title', type: 'string', label: 'Page title' },
|
||||
eventType: { name: 'event_type', type: 'string', label: 'Event type' },
|
||||
eventName: { name: 'event_name', type: 'string', label: 'Event name' },
|
||||
};
|
||||
|
||||
export const SESSION_FIELDS = {
|
||||
sessionId: { name: 'session_id', type: 'uuid' },
|
||||
websiteId: { name: 'website_id', type: 'uuid' },
|
||||
hostname: { name: 'hostname', type: 'string' },
|
||||
browser: { name: 'browser', type: 'string' },
|
||||
os: { name: 'os', type: 'string' },
|
||||
device: { name: 'device', type: 'string' },
|
||||
screen: { name: 'screen', type: 'string' },
|
||||
language: { name: 'language', type: 'string' },
|
||||
country: { name: 'country', type: 'string' },
|
||||
subdivision1: { name: 'subdivision1', type: 'string' },
|
||||
subdivision2: { name: 'subdivision2', type: 'string' },
|
||||
city: { name: 'city', type: 'string' },
|
||||
createdAt: { name: 'created_at', type: 'date' },
|
||||
};
|
||||
|
||||
export const THEME_COLORS = {
|
||||
light: {
|
||||
primary: '#2680eb',
|
||||
|
@ -1,7 +1,6 @@
|
||||
import crypto from 'crypto';
|
||||
import { v4, v5 } from 'uuid';
|
||||
import { startOfMonth } from 'date-fns';
|
||||
import { hash } from 'next-basics';
|
||||
import { v4, v5, validate } from 'uuid';
|
||||
|
||||
export function secret() {
|
||||
return hash(process.env.APP_SECRET || process.env.DATABASE_URL);
|
||||
@ -16,9 +15,9 @@ export function salt() {
|
||||
export function uuid(...args) {
|
||||
if (!args.length) return v4();
|
||||
|
||||
return v5(hash(...args, salt()), v5.DNS);
|
||||
return v5(hash(...args), v5.DNS);
|
||||
}
|
||||
|
||||
export function md5(...args) {
|
||||
return crypto.createHash('md5').update(args.join('')).digest('hex');
|
||||
export function isUuid(value) {
|
||||
return validate(value);
|
||||
}
|
||||
|
75
lib/date.js
75
lib/date.js
@ -26,10 +26,20 @@ import {
|
||||
differenceInCalendarMonths,
|
||||
differenceInCalendarYears,
|
||||
format,
|
||||
parseISO,
|
||||
max,
|
||||
min,
|
||||
isDate,
|
||||
} from 'date-fns';
|
||||
import { getDateLocale } from 'lib/lang';
|
||||
|
||||
const dateFuncs = {
|
||||
minute: [differenceInMinutes, addMinutes, startOfMinute],
|
||||
hour: [differenceInHours, addHours, startOfHour],
|
||||
day: [differenceInCalendarDays, addDays, startOfDay],
|
||||
month: [differenceInCalendarMonths, addMonths, startOfMonth],
|
||||
year: [differenceInCalendarYears, addYears, startOfYear],
|
||||
};
|
||||
|
||||
export function getTimezone() {
|
||||
return moment.tz.guess();
|
||||
}
|
||||
@ -43,11 +53,19 @@ export function parseDateRange(value, locale = 'en-US') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value?.startsWith?.('range')) {
|
||||
const [, startAt, endAt] = value.split(':');
|
||||
if (value === 'all') {
|
||||
return {
|
||||
startDate: new Date(0),
|
||||
endDate: new Date(1),
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
const startDate = new Date(+startAt);
|
||||
const endDate = new Date(+endAt);
|
||||
if (value?.startsWith?.('range')) {
|
||||
const [, startTime, endTime] = value.split(':');
|
||||
|
||||
const startDate = new Date(+startTime);
|
||||
const endDate = new Date(+endTime);
|
||||
|
||||
return {
|
||||
...getDateRangeValues(startDate, endDate),
|
||||
@ -148,17 +166,34 @@ export function parseDateRange(value, locale = 'en-US') {
|
||||
}
|
||||
}
|
||||
|
||||
export function getDateRangeValues(startDate, endDate) {
|
||||
let unit = 'year';
|
||||
if (differenceInHours(endDate, startDate) <= 48) {
|
||||
unit = 'hour';
|
||||
export function getAllowedUnits(startDate, endDate) {
|
||||
const units = ['minute', 'hour', 'day', 'month', 'year'];
|
||||
const minUnit = getMinimumUnit(startDate, endDate);
|
||||
const index = units.indexOf(minUnit);
|
||||
|
||||
return index >= 0 ? units.splice(index) : [];
|
||||
}
|
||||
|
||||
export function getMinimumUnit(startDate, endDate) {
|
||||
if (differenceInMinutes(endDate, startDate) <= 60) {
|
||||
return 'minute';
|
||||
} else if (differenceInHours(endDate, startDate) <= 48) {
|
||||
return 'hour';
|
||||
} else if (differenceInCalendarDays(endDate, startDate) <= 90) {
|
||||
unit = 'day';
|
||||
return 'day';
|
||||
} else if (differenceInCalendarMonths(endDate, startDate) <= 24) {
|
||||
unit = 'month';
|
||||
return 'month';
|
||||
}
|
||||
|
||||
return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit };
|
||||
return 'year';
|
||||
}
|
||||
|
||||
export function getDateRangeValues(startDate, endDate) {
|
||||
return {
|
||||
startDate: startOfDay(startDate),
|
||||
endDate: endOfDay(endDate),
|
||||
unit: getMinimumUnit(startDate, endDate),
|
||||
};
|
||||
}
|
||||
|
||||
export function getDateFromString(str) {
|
||||
@ -174,14 +209,6 @@ export function getDateFromString(str) {
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
const dateFuncs = {
|
||||
minute: [differenceInMinutes, addMinutes, startOfMinute],
|
||||
hour: [differenceInHours, addHours, startOfHour],
|
||||
day: [differenceInCalendarDays, addDays, startOfDay],
|
||||
month: [differenceInCalendarMonths, addMonths, startOfMonth],
|
||||
year: [differenceInCalendarYears, addYears, startOfYear],
|
||||
};
|
||||
|
||||
export function getDateArray(data, startDate, endDate, unit) {
|
||||
const arr = [];
|
||||
const [diff, add, normalize] = dateFuncs[unit];
|
||||
@ -227,3 +254,11 @@ export function dateFormat(date, str, locale = 'en-US') {
|
||||
locale: getDateLocale(locale),
|
||||
});
|
||||
}
|
||||
|
||||
export function maxDate(...args) {
|
||||
return max(args.filter(n => isDate(n)));
|
||||
}
|
||||
|
||||
export function minDate(...args) {
|
||||
return min(args.filter(n => isDate(n)));
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user