mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 13:17:19 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
6bd9916310
161 changed files with 4117 additions and 1753 deletions
|
|
@ -3,12 +3,11 @@ import { useState } from 'react';
|
|||
import MobileMenu from './MobileMenu';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
|
||||
export function HamburgerButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [active, setActive] = useState(false);
|
||||
const { cloudMode } = useConfig();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function SettingsTable({
|
|||
onPageSizeChange,
|
||||
filterValue,
|
||||
}) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [filter, setFilter] = useState(filterValue);
|
||||
const { data: value, page, count, pageSize } = data;
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ export function SettingsTable({
|
|||
delay={1000}
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
placeholder="Search"
|
||||
placeholder={formatMessage(labels.search)}
|
||||
style={{ maxWidth: '300px', marginBottom: '10px' }}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button, Row, Column } from 'react-basics';
|
||||
import { setItem } from 'next-basics';
|
||||
import useStore, { checkVersion } from 'store/version';
|
||||
|
|
@ -44,7 +45,7 @@ export function UpdateNotice({ user, config }) {
|
|||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
return createPortal(
|
||||
<Row className={styles.notice}>
|
||||
<Column variant="two" className={styles.message}>
|
||||
{formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}
|
||||
|
|
@ -55,7 +56,8 @@ export function UpdateNotice({ user, config }) {
|
|||
</Button>
|
||||
<Button onClick={handleDismissClick}>{formatMessage(labels.dismiss)}</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
</Row>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
position: absolute;
|
||||
max-width: 800px;
|
||||
gap: 20px;
|
||||
margin: 20px auto;
|
||||
justify-self: center;
|
||||
margin: 80px auto;
|
||||
align-self: center;
|
||||
background: var(--base50);
|
||||
padding: 20px;
|
||||
border: 1px solid var(--base300);
|
||||
border-radius: var(--border-radius);
|
||||
z-index: var(--z-index-popup);
|
||||
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants';
|
|||
import useTheme from 'components/hooks/useTheme';
|
||||
import useCountryNames from 'components/hooks/useCountryNames';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import styles from './WorldMap.module.css';
|
||||
|
|
@ -17,7 +18,9 @@ export function WorldMap({ data, className }) {
|
|||
const [tooltip, setTooltipPopup] = useState();
|
||||
const { theme, colors } = useTheme();
|
||||
const { locale } = useLocale();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const visitorsLabel = formatMessage(labels.visitors).toLocaleLowerCase(locale);
|
||||
const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]);
|
||||
|
||||
function getFillColor(code) {
|
||||
|
|
@ -40,7 +43,7 @@ export function WorldMap({ data, className }) {
|
|||
function handleHover(code) {
|
||||
if (code === 'AQ') return;
|
||||
const country = metrics?.find(({ x }) => x === code);
|
||||
setTooltipPopup(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} visitors`);
|
||||
setTooltipPopup(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} ${visitorsLabel}`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@ let loading = false;
|
|||
export function useConfig() {
|
||||
const { config } = useStore();
|
||||
const { get } = useApi();
|
||||
const configUrl = process.env.configUrl;
|
||||
|
||||
async function loadConfig() {
|
||||
const data = await get('/config');
|
||||
const data = await get(configUrl);
|
||||
loading = false;
|
||||
setConfig(data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!config && !loading) {
|
||||
if (!config && !loading && configUrl) {
|
||||
loading = true;
|
||||
loadConfig();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@ import { produce } from 'immer';
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTimezone } from './useTimezone';
|
||||
import useApi from './useApi';
|
||||
|
||||
const baseParameters = {
|
||||
name: 'Untitled',
|
||||
description: '',
|
||||
parameters: {},
|
||||
};
|
||||
import useMessages from './useMessages';
|
||||
|
||||
export function useReport(reportId, defaultParameters) {
|
||||
const [report, setReport] = useState(null);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const { get, post } = useApi();
|
||||
const [timezone] = useTimezone();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const baseParameters = {
|
||||
name: formatMessage(labels.untitled),
|
||||
description: '',
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const loadReport = async id => {
|
||||
const data = await get(`/reports/${id}`);
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@ import { useRouter } from 'next/router';
|
|||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
import styles from './ProfileButton.module.css';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import { CURRENT_VERSION } from 'lib/constants';
|
||||
import styles from './ProfileButton.module.css';
|
||||
|
||||
export function ProfileButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { cloudMode } = useConfig();
|
||||
const router = useRouter();
|
||||
const { dir } = useLocale();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
|
||||
const handleSelect = key => {
|
||||
if (key === 'profile') {
|
||||
|
|
@ -52,6 +52,7 @@ export function ProfileButton() {
|
|||
<Text>{formatMessage(labels.logout)}</Text>
|
||||
</Item>
|
||||
)}
|
||||
<div className={styles.version}>{`v${CURRENT_VERSION}`}</div>
|
||||
</Menu>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
|
|
|
|||
|
|
@ -8,3 +8,11 @@
|
|||
gap: 12px;
|
||||
background: var(--base50);
|
||||
}
|
||||
|
||||
.version {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
color: var(--base600);
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export function AppLayout({ title, children }) {
|
|||
const { user } = useRequireLogin();
|
||||
const config = useConfig();
|
||||
|
||||
if (!user || !config) {
|
||||
if (!user || !config || config?.uiDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,24 @@
|
|||
import { Icon, Text, Row, Column } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import classNames from 'classnames';
|
||||
import Icons from 'components/icons';
|
||||
import ThemeButton from 'components/input/ThemeButton';
|
||||
import LanguageButton from 'components/input/LanguageButton';
|
||||
import ProfileButton from 'components/input/ProfileButton';
|
||||
import styles from './NavBar.module.css';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useRouter } from 'next/router';
|
||||
import HamburgerButton from '../common/HamburgerButton';
|
||||
import HamburgerButton from 'components/common/HamburgerButton';
|
||||
import styles from './NavBar.module.css';
|
||||
|
||||
export function NavBar() {
|
||||
const { pathname } = useRouter();
|
||||
const { cloudMode } = useConfig();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const links = [
|
||||
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
||||
{ label: formatMessage(labels.websites), url: '/websites' },
|
||||
{ label: formatMessage(labels.reports), url: '/reports' },
|
||||
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
|
||||
{ label: formatMessage(labels.settings), url: '/settings' },
|
||||
].filter(n => n);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -37,10 +37,6 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-basis: 100%;
|
||||
order: -1;
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ import { useRouter } from 'next/router';
|
|||
import SideNav from './SideNav';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
import styles from './SettingsLayout.module.css';
|
||||
|
||||
export function SettingsLayout({ children }) {
|
||||
const { user } = useUser();
|
||||
const { pathname } = useRouter();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { cloudMode } = useConfig();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
|
||||
const items = [
|
||||
{ key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' },
|
||||
|
|
|
|||
|
|
@ -13,4 +13,8 @@
|
|||
.menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,10 @@ export const labels = defineMessages({
|
|||
reports: { id: 'label.reports', defaultMessage: 'Reports' },
|
||||
eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
|
||||
funnel: { id: 'label.funnel', defaultMessage: 'Funnel' },
|
||||
funnelDescription: {
|
||||
id: 'label.funnel-description',
|
||||
defaultMessage: 'Understand the conversion and drop-off rate of users.',
|
||||
},
|
||||
url: { id: 'label.url', defaultMessage: 'URL' },
|
||||
urls: { id: 'label.urls', defaultMessage: 'URLs' },
|
||||
add: { id: 'label.add', defaultMessage: 'Add' },
|
||||
|
|
@ -167,7 +171,15 @@ export const labels = defineMessages({
|
|||
overview: { id: 'label.overview', defaultMessage: 'Overview' },
|
||||
totalRecords: { id: 'label.total-records', defaultMessage: 'Total records' },
|
||||
insights: { id: 'label.insights', defaultMessage: 'Insights' },
|
||||
insightsDescription: {
|
||||
id: 'label.insights-description',
|
||||
defaultMessage: 'Dive deeper into your data by using segments and filters.',
|
||||
},
|
||||
retention: { id: 'label.retention', defaultMessage: 'Retention' },
|
||||
retentionDescription: {
|
||||
id: 'label.retention-description',
|
||||
defaultMessage: 'Measure your website stickiness by tracking how often users return.',
|
||||
},
|
||||
dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' },
|
||||
referrer: { id: 'label.referrer', defaultMessage: 'Referrer' },
|
||||
country: { id: 'label.country', defaultMessage: 'Country' },
|
||||
|
|
@ -179,6 +191,8 @@ export const labels = defineMessages({
|
|||
day: { id: 'label.day', defaultMessage: 'Day' },
|
||||
date: { id: 'label.date', defaultMessage: 'Date' },
|
||||
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
|
||||
create: { id: 'label.create', defaultMessage: 'Create' },
|
||||
search: { id: 'label.search', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@ export function CitiesTable({ websiteId, ...props }) {
|
|||
<FilterLink id="city" value={city} label={renderLabel(city, country)}>
|
||||
{country && (
|
||||
<img
|
||||
src={`${basePath}/images/flags/${
|
||||
country?.split?.('-')?.[0]?.toLowerCase() || 'xx'
|
||||
}.png`}
|
||||
src={`${basePath}/images/flags/${country?.toLowerCase() || 'xx'}.png`}
|
||||
alt={country}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -54,21 +54,18 @@ export function MetricsTable({
|
|||
city,
|
||||
},
|
||||
],
|
||||
() =>
|
||||
get(`/websites/${websiteId}/metrics`, {
|
||||
() => {
|
||||
const filters = { url, title, referrer, os, browser, device, country, region, city };
|
||||
|
||||
filters[type] = undefined;
|
||||
|
||||
return get(`/websites/${websiteId}/metrics`, {
|
||||
type,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
url,
|
||||
title,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
}),
|
||||
...filters,
|
||||
});
|
||||
},
|
||||
{ onSuccess: onDataLoad, retryDelay: delay || DEFAULT_ANIMATION_DURATION },
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,17 +13,15 @@ export function RegionsTable({ websiteId, ...props }) {
|
|||
const countryNames = useCountryNames(locale);
|
||||
const { basePath } = useRouter();
|
||||
|
||||
const renderLabel = x => {
|
||||
return regions[x] ? `${regions[x]}, ${countryNames[x.split('-')[0]]}` : x;
|
||||
const renderLabel = (code, country) => {
|
||||
const region = code.includes('-') ? code : `${country}-${code}`;
|
||||
return regions[region] ? `${regions[region]}, ${countryNames[country]}` : region;
|
||||
};
|
||||
|
||||
const renderLink = ({ x: code }) => {
|
||||
const renderLink = ({ x: code, country }) => {
|
||||
return (
|
||||
<FilterLink id="region" className={locale} value={code} label={renderLabel(code)}>
|
||||
<img
|
||||
src={`${basePath}/images/flags/${code?.split('-')?.[0]?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
<FilterLink id="region" className={locale} value={code} label={renderLabel(code, country)}>
|
||||
<img src={`${basePath}/images/flags/${country?.toLowerCase() || 'xx'}.png`} alt={code} />
|
||||
</FilterLink>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, Icon, Icons, Text, Flexbox } from 'react-basics';
|
||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Pager from 'components/common/Pager';
|
||||
import WebsiteChartList from 'components/pages/websites/WebsiteChartList';
|
||||
import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton';
|
||||
import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
|
||||
|
|
@ -11,23 +11,24 @@ import useApi from 'components/hooks/useApi';
|
|||
import useDashboard from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useApiFilter from 'components/hooks/useApiFilter';
|
||||
|
||||
export function Dashboard() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const dashboard = useDashboard();
|
||||
const { showCharts, limit, editing } = dashboard;
|
||||
const [max, setMax] = useState(limit);
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['websites'], () =>
|
||||
get('/websites', { includeTeams: 1 }),
|
||||
);
|
||||
const hasData = data && data?.data.length !== 0;
|
||||
|
||||
const { showCharts, editing } = useDashboard();
|
||||
const { dir } = useLocale();
|
||||
|
||||
function handleMore() {
|
||||
setMax(max + limit);
|
||||
}
|
||||
const { get, useQuery } = useApi();
|
||||
const { page, handlePageChange } = useApiFilter();
|
||||
const pageSize = 10;
|
||||
const {
|
||||
data: result,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery(['websites', page, pageSize], () =>
|
||||
get('/websites', { includeTeams: 1, page, pageSize }),
|
||||
);
|
||||
const { data, count } = result || {};
|
||||
const hasData = data && data?.length !== 0;
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
|
|
@ -48,19 +49,17 @@ export function Dashboard() {
|
|||
)}
|
||||
{hasData && (
|
||||
<>
|
||||
{editing && <DashboardEdit websites={data?.data} />}
|
||||
{editing && <DashboardEdit />}
|
||||
{!editing && (
|
||||
<WebsiteChartList websites={data?.data} showCharts={showCharts} limit={max} />
|
||||
)}
|
||||
{max < data.length && (
|
||||
<Flexbox justifyContent="center">
|
||||
<Button onClick={handleMore}>
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.More />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.more)}</Text>
|
||||
</Button>
|
||||
</Flexbox>
|
||||
<>
|
||||
<WebsiteChartList websites={data} showCharts={showCharts} limit={pageSize} />
|
||||
<Pager
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,23 +5,33 @@ import { Button } from 'react-basics';
|
|||
import { firstBy } from 'thenby';
|
||||
import useDashboard, { saveDashboard } from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import styles from './DashboardEdit.module.css';
|
||||
import Page from 'components/layout/Page';
|
||||
|
||||
const dragId = 'dashboard-website-ordering';
|
||||
|
||||
export function DashboardEdit({ websites }) {
|
||||
export function DashboardEdit() {
|
||||
const settings = useDashboard();
|
||||
const { websiteOrder } = settings;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [order, setOrder] = useState(websiteOrder || []);
|
||||
const { get, useQuery } = useApi();
|
||||
const {
|
||||
data: result,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
|
||||
const { data: websites } = result || {};
|
||||
|
||||
const ordered = useMemo(
|
||||
() =>
|
||||
websites
|
||||
const ordered = useMemo(() => {
|
||||
if (websites) {
|
||||
return websites
|
||||
.map(website => ({ ...website, order: order.indexOf(website.id) }))
|
||||
.sort(firstBy('order')),
|
||||
[websites, order],
|
||||
);
|
||||
.sort(firstBy('order'));
|
||||
}
|
||||
return [];
|
||||
}, [websites, order]);
|
||||
|
||||
function handleWebsiteDrag({ destination, source }) {
|
||||
if (!destination || destination.index === source.index) return;
|
||||
|
|
@ -49,7 +59,7 @@ export function DashboardEdit({ websites }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page loading={isLoading} error={error}>
|
||||
<div className={styles.buttons}>
|
||||
<Button onClick={handleSave} variant="action" size="small">
|
||||
{formatMessage(labels.save)}
|
||||
|
|
@ -95,7 +105,7 @@ export function DashboardEdit({ websites }) {
|
|||
</Droppable>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function FieldFilterForm({
|
|||
type,
|
||||
values,
|
||||
onSelect,
|
||||
includeOnlyEquals,
|
||||
allowFilterSelect = true,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [filter, setFilter] = useState('eq');
|
||||
|
|
@ -34,7 +34,7 @@ export default function FieldFilterForm({
|
|||
<Form>
|
||||
<FormRow label={label} className={styles.filter}>
|
||||
<Flexbox gap={10}>
|
||||
{!includeOnlyEquals && (
|
||||
{allowFilterSelect && (
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={filters}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function useValues(websiteId, type) {
|
|||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export default function FilterSelectForm({ websiteId, items, onSelect, includeOnlyEquals }) {
|
||||
export default function FilterSelectForm({ websiteId, items, onSelect, allowFilterSelect }) {
|
||||
const [field, setField] = useState();
|
||||
const { data, isLoading } = useValues(websiteId, field?.name);
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ export default function FilterSelectForm({ websiteId, items, onSelect, includeOn
|
|||
type={field?.type}
|
||||
values={data}
|
||||
onSelect={onSelect}
|
||||
includeOnlyEquals={includeOnlyEquals}
|
||||
allowFilterSelect={allowFilterSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export function ReportHeader({ icon }) {
|
|||
|
||||
const { name, description, parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const defaultName = formatMessage(labels.untitled);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!report.id) {
|
||||
|
|
@ -39,7 +40,7 @@ export function ReportHeader({ icon }) {
|
|||
};
|
||||
|
||||
const handleNameChange = name => {
|
||||
updateReport({ name: name || 'Untitled' });
|
||||
updateReport({ name: name || defaultName });
|
||||
};
|
||||
|
||||
const handleDescriptionChange = description => {
|
||||
|
|
@ -54,7 +55,7 @@ export function ReportHeader({ icon }) {
|
|||
key={name}
|
||||
name="name"
|
||||
value={name}
|
||||
placeholder={formatMessage(labels.untitled)}
|
||||
placeholder={defaultName}
|
||||
onCommit={handleNameChange}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import styles from './ReportTemplates.module.css';
|
|||
import { useMessages } from 'components/hooks';
|
||||
|
||||
function ReportItem({ title, description, url, icon }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<div className={styles.report}>
|
||||
<div className={styles.title}>
|
||||
|
|
@ -22,7 +24,7 @@ function ReportItem({ title, description, url, icon }) {
|
|||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>Create</Text>
|
||||
<Text>{formatMessage(labels.create)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -36,19 +38,19 @@ export function ReportTemplates({ showHeader = true }) {
|
|||
const reports = [
|
||||
{
|
||||
title: formatMessage(labels.insights),
|
||||
description: 'Dive deeper into your data by using segments and filters.',
|
||||
description: formatMessage(labels.insightsDescription),
|
||||
url: '/reports/insights',
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.funnel),
|
||||
description: 'Understand the conversion and drop-off rate of users.',
|
||||
description: formatMessage(labels.funnelDescription),
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.retention),
|
||||
description: 'Measure you website stickiness by tracking how often users return.',
|
||||
description: formatMessage(labels.retentionDescription),
|
||||
url: '/reports/retention',
|
||||
icon: <Magnet />,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useMessages } from 'components/hooks';
|
|||
import useUser from 'components/hooks/useUser';
|
||||
import { useState } from 'react';
|
||||
import { Button, Flexbox, Icon, Icons, Modal, Text } from 'react-basics';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
export function ReportsTable({
|
||||
data = [],
|
||||
|
|
@ -34,6 +35,15 @@ export function ReportsTable({
|
|||
{ name: 'action', label: ' ' },
|
||||
];
|
||||
|
||||
const cellRender = (row, data, key) => {
|
||||
if (key === 'type') {
|
||||
return formatMessage(
|
||||
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)],
|
||||
);
|
||||
}
|
||||
return data[key];
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
onDelete(report.id);
|
||||
};
|
||||
|
|
@ -42,6 +52,7 @@ export function ReportsTable({
|
|||
<>
|
||||
<SettingsTable
|
||||
columns={columns}
|
||||
cellRender={cellRender}
|
||||
data={data}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export function EventDataParameters() {
|
|||
);
|
||||
})}
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={!queryEnabled} loading={isRunning}>
|
||||
<SubmitButton variant="primary" disabled={!queryEnabled} isLoading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export function FunnelParameters() {
|
|||
<ParameterList items={urls} onRemove={handleRemoveUrl} />
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export function InsightsParameters() {
|
|||
);
|
||||
})}
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={!queryEnabled} loading={isRunning}>
|
||||
<SubmitButton variant="primary" disabled={!queryEnabled} isLoading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function RetentionParameters() {
|
|||
<MonthSelect date={startDate} onChange={handleDateChange} />
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import classNames from 'classnames';
|
|||
import { ReportContext } from '../Report';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { formatDate } from 'lib/date';
|
||||
import styles from './RetentionTable.module.css';
|
||||
|
||||
export function RetentionTable() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const { report } = useContext(ReportContext);
|
||||
const { data } = report || {};
|
||||
|
||||
|
|
@ -51,7 +53,7 @@ export function RetentionTable() {
|
|||
{rows.map(({ date, visitors, records }, rowIndex) => {
|
||||
return (
|
||||
<div key={rowIndex} className={styles.row}>
|
||||
<div className={styles.date}>{formatDate(`${date} 00:00:00`, 'PP')}</div>
|
||||
<div className={styles.date}>{formatDate(`${date} 00:00:00`, 'PP', locale)}</div>
|
||||
<div className={styles.visitors}>{visitors}</div>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ import ThemeSetting from 'components/pages/settings/profile/ThemeSetting';
|
|||
import PasswordChangeButton from './PasswordChangeButton';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
||||
export function ProfileDetails() {
|
||||
const { user } = useUser();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { cloudMode } = useConfig();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Breadcrumbs, Item, Tabs, useToasts } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import { Item, Tabs, useToasts } from 'react-basics';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
|
@ -44,16 +43,7 @@ export function TeamSettings({ teamId }) {
|
|||
|
||||
return (
|
||||
<Page loading={isLoading || !values}>
|
||||
<PageHeader
|
||||
title={
|
||||
<Breadcrumbs>
|
||||
<Item>
|
||||
<Link href="/settings/teams">{formatMessage(labels.teams)}</Link>
|
||||
</Item>
|
||||
<Item>{values?.name}</Item>
|
||||
</Breadcrumbs>
|
||||
}
|
||||
/>
|
||||
<PageHeader title={values?.name} />
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
||||
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||
<Item key="members">{formatMessage(labels.members)}</Item>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Breadcrumbs, Item, Tabs, useToasts } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import { Item, Tabs, useToasts } from 'react-basics';
|
||||
import UserEditForm from 'components/pages/settings/users/UserEditForm';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
|
|
@ -44,16 +43,7 @@ export function UserSettings({ userId }) {
|
|||
|
||||
return (
|
||||
<Page loading={isLoading || !values}>
|
||||
<PageHeader
|
||||
title={
|
||||
<Breadcrumbs>
|
||||
<Item>
|
||||
<Link href="/settings/users">{formatMessage(labels.users)}</Link>
|
||||
</Item>
|
||||
<Item>{values?.username}</Item>
|
||||
</Breadcrumbs>
|
||||
}
|
||||
/>
|
||||
<PageHeader title={values?.username} />
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
||||
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||
<Item key="websites">{formatMessage(labels.websites)}</Item>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import { TextArea } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export function TrackingCode({ websiteId }) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { basePath, trackerScriptName } = useConfig();
|
||||
const { basePath } = useRouter();
|
||||
const config = useConfig();
|
||||
|
||||
const trackerScriptName =
|
||||
config?.trackerScriptName?.split(',')?.map(n => n.trim())?.[0] || 'script.js';
|
||||
|
||||
const url = trackerScriptName?.startsWith('http')
|
||||
? trackerScriptName
|
||||
: `${location.origin}${basePath}/${
|
||||
trackerScriptName?.split(',')?.map(n => n.trim())?.[0] || 'script.js'
|
||||
}`;
|
||||
: `${process.env.analyticsUrl || location.origin}${basePath}/${trackerScriptName}`;
|
||||
|
||||
const code = `<script async src="${url}" data-website-id="${websiteId}"></script>`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Breadcrumbs, Item, Tabs, useToasts, Button, Text, Icon, Icons } from 'react-basics';
|
||||
import { Item, Tabs, useToasts, Button, Text, Icon, Icons } from 'react-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import Page from 'components/layout/Page';
|
||||
|
|
@ -49,16 +49,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) {
|
|||
|
||||
return (
|
||||
<Page loading={isLoading || !values}>
|
||||
<PageHeader
|
||||
title={
|
||||
<Breadcrumbs>
|
||||
<Item>
|
||||
<Link href="/settings/websites">{formatMessage(labels.websites)}</Link>
|
||||
</Item>
|
||||
<Item>{values?.name}</Item>
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<PageHeader title={values?.name}>
|
||||
<Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import classNames from 'classnames';
|
||||
import { useApi, useDateRange, useMessages, usePageQuery, useSticky } from 'components/hooks';
|
||||
import RefreshButton from 'components/input/RefreshButton';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
|
|
@ -10,7 +9,7 @@ import { formatShortTime } from 'lib/format';
|
|||
import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row } from 'react-basics';
|
||||
import styles from './WebsiteMetricsBar.module.css';
|
||||
|
||||
export function WebsiteMetricsBar({ websiteId, showFilter = true, showRefresh = true, sticky }) {
|
||||
export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
@ -88,7 +87,7 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, showRefresh =
|
|||
handleAddFilter(value);
|
||||
close();
|
||||
}}
|
||||
includeOnlyEquals={true}
|
||||
allowFilterSelect={false}
|
||||
/>
|
||||
</PopupForm>
|
||||
);
|
||||
|
|
@ -161,7 +160,6 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, showRefresh =
|
|||
<Column defaultSize={12} xl={4}>
|
||||
<div className={styles.actions}>
|
||||
{showFilter && <WebsiteFilterButton />}
|
||||
{showRefresh && <RefreshButton websiteId={websiteId} />}
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
</div>
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
|
|||
import WebsiteList from 'components/pages/settings/websites/WebsitesList';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
|
|
@ -24,8 +23,8 @@ export function WebsitesPage() {
|
|||
const [tab, setTab] = useState('my-websites');
|
||||
const [fetch, setFetch] = useState(1);
|
||||
const { user } = useUser();
|
||||
const { cloudMode } = useConfig();
|
||||
const { showToast } = useToasts();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
|
||||
const handleSave = async () => {
|
||||
setFetch(fetch + 1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue