Merge branch 'dev' into feat/um-202-event-data-new

pull/1841/head
Brian Cao 2023-03-23 00:39:39 -07:00
commit f4b32bab43
92 changed files with 479 additions and 479 deletions

View File

@ -1,10 +1,9 @@
import { useIntl } from 'react-intl';
import { Icon, Icons, Text } from 'react-basics'; import { Icon, Icons, Text } from 'react-basics';
import { messages } from 'components/messages';
import styles from './ErrorMessage.module.css'; import styles from './ErrorMessage.module.css';
import useMessages from 'hooks/useMessages';
export default function ErrorMessage() { export default function ErrorMessage() {
const { formatMessage } = useIntl(); const { formatMessage, messages } = useMessages();
return ( return (
<div className={styles.error}> <div className={styles.error}>

View File

@ -1,32 +0,0 @@
import { useMeasure } from 'react-basics';
import classNames from 'classnames';
import useSticky from 'hooks/useSticky';
export default function StickyHeader({
className,
stickyClassName,
stickyStyle,
enabled = true,
scrollElement,
children,
}) {
const { ref: scrollRef, isSticky } = useSticky({ scrollElement });
const { ref: measureRef, dimensions } = useMeasure();
const active = enabled && isSticky;
return (
<div
ref={measureRef}
data-sticky={active}
style={active ? { height: dimensions.height } : null}
>
<div
ref={scrollRef}
className={classNames(className, { [stickyClassName]: active })}
style={active ? { ...stickyStyle, width: dimensions.width } : null}
>
{children}
</div>
</div>
);
}

View File

@ -1,14 +1,13 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useIntl } from 'react-intl'; import { Button, Row, Column } from 'react-basics';
import { Button, Banner, Row, Column, Flexbox } from 'react-basics';
import { setItem } from 'next-basics'; import { setItem } from 'next-basics';
import useStore, { checkVersion } from 'store/version'; import useStore, { checkVersion } from 'store/version';
import { REPO_URL, VERSION_CHECK } from 'lib/constants'; import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import { labels, messages } from 'components/messages';
import styles from './UpdateNotice.module.css'; import styles from './UpdateNotice.module.css';
import useMessages from 'hooks/useMessages';
export default function UpdateNotice() { export default function UpdateNotice() {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { latest, checked, hasUpdate, releaseUrl } = useStore(); const { latest, checked, hasUpdate, releaseUrl } = useStore();
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);

View File

@ -1,17 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
import { useIntl } from 'react-intl';
import { endOfYear, isSameDay } from 'date-fns'; import { endOfYear, isSameDay } from 'date-fns';
import DatePickerForm from 'components/metrics/DatePickerForm'; import DatePickerForm from 'components/metrics/DatePickerForm';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import { dateFormat, getDateRangeValues } from 'lib/date'; import { dateFormat, getDateRangeValues } from 'lib/date';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useMessages from 'hooks/useMessages';
function DateFilter({ websiteId, value, className }) { function DateFilter({ websiteId, value, className }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { get } = useApi(); const { get } = useApi();
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate } = dateRange; const { startDate, endDate } = dateRange;

View File

@ -1,14 +1,11 @@
import { Icon, Button, PopupTrigger, Popup, Tooltip, Text } from 'react-basics'; import { Icon, Button, PopupTrigger, Popup, Text } from 'react-basics';
import { useIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { languages } from 'lib/lang'; import { languages } from 'lib/lang';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages';
import styles from './LanguageButton.module.css'; import styles from './LanguageButton.module.css';
export default function LanguageButton({ tooltipPosition = 'top', menuPosition = 'right' }) { export default function LanguageButton() {
const { formatMessage } = useIntl();
const { locale, saveLocale } = useLocale(); const { locale, saveLocale } = useLocale();
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key })); const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
@ -18,14 +15,12 @@ export default function LanguageButton({ tooltipPosition = 'top', menuPosition =
return ( return (
<PopupTrigger> <PopupTrigger>
<Tooltip label={formatMessage(labels.language)} position={tooltipPosition}>
<Button variant="quiet"> <Button variant="quiet">
<Icon> <Icon>
<Icons.Globe /> <Icons.Globe />
</Icon> </Icon>
</Button> </Button>
</Tooltip> <Popup position="bottom" alignment="end">
<Popup position={menuPosition} alignment="end">
<div className={styles.menu}> <div className={styles.menu}>
{items.map(({ value, label }) => { {items.map(({ value, label }) => {
return ( return (

View File

@ -1,10 +1,9 @@
import { Button, Icon, Icons, Tooltip } from 'react-basics'; import { Button, Icon, Icons, Tooltip } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
import { useIntl } from 'react-intl';
export default function LogoutButton({ tooltipPosition = 'top' }) { export default function LogoutButton({ tooltipPosition = 'top' }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
return ( return (
<Link href="/logout"> <Link href="/logout">
<Tooltip label={formatMessage(labels.logout)} position={tooltipPosition}> <Tooltip label={formatMessage(labels.logout)} position={tooltipPosition}>

View File

@ -0,0 +1,53 @@
import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics';
import { useRouter } from 'next/router';
import Icons from 'components/icons';
import useMessages from 'hooks/useMessages';
import useUser from 'hooks/useUser';
import styles from './ProfileButton.module.css';
export default function ProfileButton() {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const router = useRouter();
const handleSelect = key => {
if (key === 'profile') {
router.push('/settings/profile');
}
if (key === 'logout') {
router.push('/logout');
}
};
return (
<PopupTrigger>
<Button variant="quiet">
<Icon>
<Icons.Profile />
</Icon>
<Icon size="sm">
<Icons.ChevronDown />
</Icon>
</Button>
<Popup position="bottom" alignment="end">
<Menu variant="popup" onSelect={handleSelect} className={styles.menu}>
<Item key="user" className={styles.item}>
<Text>{user.username}</Text>
</Item>
<Item key="profile" className={styles.item} divider={true}>
<Icon>
<Icons.User />
</Icon>
<Text>{formatMessage(labels.profile)}</Text>
</Item>
<Item key="logout" className={styles.item}>
<Icon>
<Icons.Logout />
</Icon>
<Text>{formatMessage(labels.logout)}</Text>
</Item>
</Menu>
</Popup>
</PopupTrigger>
);
}

View File

@ -0,0 +1,9 @@
.menu {
width: 200px;
}
.item {
display: flex;
gap: 12px;
background: var(--base50);
}

View File

@ -1,12 +1,11 @@
import { useIntl } from 'react-intl';
import { LoadingButton, Icon, Tooltip } from 'react-basics'; import { LoadingButton, Icon, Tooltip } from 'react-basics';
import { setWebsiteDateRange } from 'store/websites'; import { setWebsiteDateRange } from 'store/websites';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
function RefreshButton({ websiteId, isLoading }) { function RefreshButton({ websiteId, isLoading }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
function handleClick() { function handleClick() {

View File

@ -1,13 +1,12 @@
import { useIntl } from 'react-intl';
import { Button, Icon, Tooltip, PopupTrigger, Popup, Form, FormRow } from 'react-basics'; import { Button, Icon, Tooltip, PopupTrigger, Popup, Form, FormRow } from 'react-basics';
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting'; import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting'; import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
import styles from './SettingsButton.module.css'; import styles from './SettingsButton.module.css';
export default function SettingsButton() { export default function SettingsButton() {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
return ( return (
<PopupTrigger> <PopupTrigger>

View File

@ -1,14 +1,11 @@
import { useTransition, animated } from 'react-spring'; import { useTransition, animated } from 'react-spring';
import { Button, Icon, Tooltip } from 'react-basics'; import { Button, Icon } from 'react-basics';
import { useIntl } from 'react-intl';
import useTheme from 'hooks/useTheme'; import useTheme from 'hooks/useTheme';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages';
import styles from './ThemeButton.module.css'; import styles from './ThemeButton.module.css';
export default function ThemeButton({ tooltipPosition = 'top' }) { export default function ThemeButton() {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
const { formatMessage } = useIntl();
const transitions = useTransition(theme, { const transitions = useTransition(theme, {
initial: { opacity: 1 }, initial: { opacity: 1 },
@ -28,7 +25,6 @@ export default function ThemeButton({ tooltipPosition = 'top' }) {
} }
return ( return (
<Tooltip label={formatMessage(labels.theme)} position={tooltipPosition}>
<Button variant="quiet" className={styles.button} onClick={handleClick}> <Button variant="quiet" className={styles.button} onClick={handleClick}>
{transitions((style, item) => ( {transitions((style, item) => (
<animated.div key={item} style={style}> <animated.div key={item} style={style}>
@ -36,6 +32,5 @@ export default function ThemeButton({ tooltipPosition = 'top' }) {
</animated.div> </animated.div>
))} ))}
</Button> </Button>
</Tooltip>
); );
} }

View File

@ -1,10 +1,9 @@
import { useIntl } from 'react-intl';
import { Dropdown, Item } from 'react-basics'; import { Dropdown, Item } from 'react-basics';
import { labels } from 'components/messages';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
export default function WebsiteSelect({ websiteId, onSelect }) { export default function WebsiteSelect({ websiteId, onSelect }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data } = useQuery(['websites:me'], () => get('/me/websites')); const { data } = useQuery(['websites:me'], () => get('/me/websites'));

View File

@ -1,15 +1,21 @@
.layout { .layout {
display: grid; display: grid;
grid-template-rows: 1fr; grid-template-rows: max-content 1fr;
grid-template-columns: max-content 1fr; grid-template-columns: 1fr;
} }
.nav { .nav {
grid-row: 1 / 3; height: 60px;
width: 100vw;
z-index: 100;
grid-column: 1;
grid-row: 1 / 2;
} }
.body { .body {
grid-area: 1 / 2; grid-column: 1;
overflow: auto; grid-row: 2 / 3;
height: 100vh; min-height: 0;
max-height: calc(100vh - 60px);
overflow-y: auto;
} }

View File

@ -1,69 +1,50 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl';
import { Icon, Text } from 'react-basics'; import { Icon, Text } from 'react-basics';
import Link from 'next/link';
import classNames from 'classnames'; import classNames from 'classnames';
import Icons from 'components/icons'; import Icons from 'components/icons';
import ThemeButton from 'components/input/ThemeButton'; import ThemeButton from 'components/input/ThemeButton';
import LanguageButton from 'components/input/LanguageButton'; import LanguageButton from 'components/input/LanguageButton';
import LogoutButton from 'components/input/LogoutButton'; import ProfileButton from 'components/input/ProfileButton';
import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
import NavGroup from './NavGroup';
import styles from './NavBar.module.css'; import styles from './NavBar.module.css';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
import useMessages from 'hooks/useMessages';
export default function NavBar() { export default function NavBar() {
const { user } = useUser();
const { cloudMode } = useConfig(); const { cloudMode } = useConfig();
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const [minimized, setMinimized] = useState(false); const [minimized, setMinimized] = useState(false);
const tooltipPosition = minimized ? 'right' : 'top';
const analytics = [ const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Icons.Dashboard /> }, { label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Icons.Dashboard /> },
{ label: formatMessage(labels.realtime), url: '/realtime', icon: <Icons.Clock /> }, { label: formatMessage(labels.realtime), url: '/realtime', icon: <Icons.Clock /> },
]; !cloudMode && { label: formatMessage(labels.settings), url: '/settings', icon: <Icons.Gear /> },
const settings = [
!cloudMode && {
label: formatMessage(labels.websites),
url: '/settings/websites',
icon: <Icons.Globe />,
},
user?.isAdmin && {
label: formatMessage(labels.users),
url: '/settings/users',
icon: <Icons.User />,
},
!cloudMode && {
label: formatMessage(labels.teams),
url: '/settings/teams',
icon: <Icons.Users />,
},
{ label: formatMessage(labels.profile), url: '/settings/profile', icon: <Icons.Profile /> },
].filter(n => n); ].filter(n => n);
const handleMinimize = () => setMinimized(state => !state); const handleMinimize = () => setMinimized(state => !state);
return ( return (
<div className={classNames(styles.navbar, { [styles.minimized]: minimized })}> <div className={classNames(styles.navbar, { [styles.minimized]: minimized })}>
<div className={styles.header} onClick={handleMinimize}> <div className={styles.logo} onClick={handleMinimize}>
<Icon size="lg"> <Icon size="lg">
<Icons.Logo /> <Icons.Logo />
</Icon> </Icon>
<Text className={styles.text}>umami</Text> <Text className={styles.text}>umami</Text>
<Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}>
<Icons.ChevronDown />
</Icon>
</div> </div>
<NavGroup title={formatMessage(labels.analytics)} items={analytics} minimized={minimized} /> <div className={styles.links}>
<NavGroup title={formatMessage(labels.settings)} items={settings} minimized={minimized} /> {links.map(({ url, icon, label }) => {
<div className={styles.footer}> return (
<div className={styles.buttons}> <Link key={url} href={url}>
<ThemeButton tooltipPosition={tooltipPosition} /> <Icon>{icon}</Icon>
<LanguageButton tooltipPosition={tooltipPosition} /> <Text>{label}</Text>
{!cloudMode && <LogoutButton tooltipPosition={tooltipPosition} />} </Link>
);
})}
</div> </div>
<div className={styles.actions}>
<ThemeButton />
<LanguageButton />
{!cloudMode && <ProfileButton />}
</div> </div>
</div> </div>
); );

View File

@ -1,62 +1,49 @@
.navbar { .navbar {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: center; align-items: center;
height: 60px;
background: var(--base75); background: var(--base75);
height: 100%; border-bottom: 1px solid var(--base200);
width: 200px; padding: 0 20px;
border-right: 2px solid var(--base200);
} }
.header { .logo {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 10px; gap: 10px;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
padding: 20px 0;
cursor: pointer; cursor: pointer;
min-width: 0;
} }
.header:hover .icon { .links {
visibility: visible;
}
.icon {
visibility: hidden;
position: absolute;
right: -10px;
border-radius: 100%;
color: var(--base50);
background: var(--base800);
height: 20px;
width: 20px;
}
.minimized.navbar {
width: 60px;
}
.minimized .text {
display: none;
}
.footer {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
justify-content: flex-end; gap: 20px;
padding: 20px; padding: 0 40px;
flex: 1;
font-weight: 700;
} }
.buttons { .links a {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 10px;
color: var(--font-color100);
} }
.minimized .buttons { .links a:hover {
flex-direction: column; color: var(--primary400);
}
.actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
min-width: 0;
} }

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './Page.module.css';
import { Banner, Loading } from 'react-basics'; import { Banner, Loading } from 'react-basics';
import styles from './Page.module.css';
export default function Page({ className, error, loading, children }) { export default function Page({ className, error, loading, children }) {
if (error) { if (error) {

View File

@ -109,6 +109,8 @@ export const labels = defineMessages({
laptop: { id: 'label.laptop', defaultMessage: 'Laptop' }, laptop: { id: 'label.laptop', defaultMessage: 'Laptop' },
tablet: { id: 'label.tablet', defaultMessage: 'Tablet' }, tablet: { id: 'label.tablet', defaultMessage: 'Tablet' },
mobile: { id: 'label.mobile', defaultMessage: 'Mobile' }, mobile: { id: 'label.mobile', defaultMessage: 'Mobile' },
toggleCharts: { id: 'label.toggle-charts', defaultMessage: 'Toggle charts' },
editDashboard: { id: 'label.edit-dashboard', defaultMessage: 'Edit dashboard' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({
@ -201,9 +203,3 @@ export const messages = defineMessages({
defaultMessage: '{event} on {url}', defaultMessage: '{event} on {url}',
}, },
}); });
export function getMessage(id, formatMessage) {
const message = Object.values(messages).find(value => value.id === id);
return message ? formatMessage(message) : id;
}

View File

@ -1,12 +1,11 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { StatusLight } from 'react-basics'; import { StatusLight } from 'react-basics';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
import styles from './ActiveUsers.module.css'; import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) { export default function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
const { formatMessage } = useIntl(); const { formatMessage, messages } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data } = useQuery( const { data } = useQuery(
['websites:active', websiteId], ['websites:active', websiteId],

View File

@ -1,20 +1,14 @@
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import { useIntl, defineMessages } from 'react-intl';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useMessages from 'hooks/useMessages';
const messages = defineMessages({
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
countries: { id: 'metrics.countries', defaultMessage: 'Countries' },
visitors: { id: 'metrics.visitors', defaultMessage: 'Visitors' },
});
export default function CountriesTable({ websiteId, onDataLoad, ...props }) { export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
const { locale } = useLocale(); const { locale } = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
function renderLink({ x: code }) { function renderLink({ x: code }) {
return ( return (
@ -22,7 +16,7 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
<FilterLink <FilterLink
id="country" id="country"
value={code} value={code}
label={countryNames[code] ?? formatMessage(messages.unknown)} label={countryNames[code] ?? formatMessage(labels.unknown)}
/> />
</div> </div>
); );
@ -31,9 +25,9 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
return ( return (
<MetricsTable <MetricsTable
{...props} {...props}
title={formatMessage(messages.countries)} title={formatMessage(labels.countries)}
type="country" type="country"
metric={formatMessage(messages.visitors)} metric={formatMessage(labels.visitors)}
websiteId={websiteId} websiteId={websiteId}
onDataLoad={data => onDataLoad?.(percentFilter(data))} onDataLoad={data => onDataLoad?.(percentFilter(data))}
renderLabel={renderLink} renderLabel={renderLink}

View File

@ -1,13 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, ButtonGroup, Calendar } from 'react-basics'; import { Button, ButtonGroup, Calendar } from 'react-basics';
import { useIntl } from 'react-intl';
import { isAfter, isBefore, isSameDay } from 'date-fns'; import { isAfter, isBefore, isSameDay } from 'date-fns';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import { getDateRangeValues } from 'lib/date'; import { getDateRangeValues } from 'lib/date';
import { getDateLocale } from 'lib/lang'; import { getDateLocale } from 'lib/lang';
import { labels } from 'components/messages';
import styles from './DatePickerForm.module.css';
import { FILTER_DAY, FILTER_RANGE } from 'lib/constants'; import { FILTER_DAY, FILTER_RANGE } from 'lib/constants';
import useMessages from 'hooks/useMessages';
import styles from './DatePickerForm.module.css';
export default function DatePickerForm({ export default function DatePickerForm({
startDate: defaultStartDate, startDate: defaultStartDate,
@ -24,7 +23,7 @@ export default function DatePickerForm({
const [startDate, setStartDate] = useState(defaultStartDate); const [startDate, setStartDate] = useState(defaultStartDate);
const [endDate, setEndDate] = useState(defaultEndDate); const [endDate, setEndDate] = useState(defaultEndDate);
const { locale } = useLocale(); const { locale } = useLocale();
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const disabled = const disabled =
selected === FILTER_DAY selected === FILTER_DAY

View File

@ -1,10 +1,9 @@
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { useIntl } from 'react-intl';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function DevicesTable({ websiteId, ...props }) { export default function DevicesTable({ websiteId, ...props }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
function renderLink({ x: device }) { function renderLink({ x: device }) {
return ( return (

View File

@ -1,13 +1,8 @@
import { defineMessages, useIntl } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import useMessages from 'hooks/useMessages';
const messages = defineMessages({
events: { id: 'metrics.events', defaultMessage: 'Events' },
actions: { id: 'metrics.actions', defaultMessage: 'Actions' },
});
export default function EventsTable({ websiteId, ...props }) { export default function EventsTable({ websiteId, ...props }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
function handleDataLoad(data) { function handleDataLoad(data) {
props.onDataLoad?.(data); props.onDataLoad?.(data);
@ -16,9 +11,9 @@ export default function EventsTable({ websiteId, ...props }) {
return ( return (
<MetricsTable <MetricsTable
{...props} {...props}
title={formatMessage(messages.events)} title={formatMessage(labels.events)}
type="event" type="event"
metric={formatMessage(messages.actions)} metric={formatMessage(labels.actions)}
websiteId={websiteId} websiteId={websiteId}
onDataLoad={handleDataLoad} onDataLoad={handleDataLoad}
/> />

View File

@ -1,12 +1,11 @@
import { useIntl } from 'react-intl';
import { safeDecodeURI } from 'next-basics'; import { safeDecodeURI } from 'next-basics';
import { Button, Icon, Icons, Text } from 'react-basics'; import { Button, Icon, Icons, Text } from 'react-basics';
import { labels } from 'components/messages';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import styles from './FilterTags.module.css'; import styles from './FilterTags.module.css';
import useMessages from 'hooks/useMessages';
export default function FilterTags({ websiteId, params, onClick }) { export default function FilterTags({ websiteId, params }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { const {
router, router,
resolveUrl, resolveUrl,

View File

@ -1,17 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { Loading } from 'react-basics'; import { Loading } from 'react-basics';
import { useIntl } from 'react-intl';
import ErrorMessage from 'components/common/ErrorMessage'; import ErrorMessage from 'components/common/ErrorMessage';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format'; import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard'; import MetricCard from './MetricCard';
import { labels } from 'components/messages';
import styles from './MetricsBar.module.css'; import styles from './MetricsBar.module.css';
import useMessages from 'hooks/useMessages';
export default function MetricsBar({ websiteId }) { export default function MetricsBar({ websiteId }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange; const { startDate, endDate, modified } = dateRange;

View File

@ -1,6 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Loading, Icon, Text, Button } from 'react-basics'; import { Loading, Icon, Text, Button } from 'react-basics';
import { defineMessages, useIntl } from 'react-intl';
import Link from 'next/link'; import Link from 'next/link';
import firstBy from 'thenby'; import firstBy from 'thenby';
import classNames from 'classnames'; import classNames from 'classnames';
@ -12,12 +11,9 @@ import ErrorMessage from 'components/common/ErrorMessage';
import DataTable from './DataTable'; import DataTable from './DataTable';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import Icons from 'components/icons'; import Icons from 'components/icons';
import useMessages from 'hooks/useMessages';
import styles from './MetricsTable.module.css'; import styles from './MetricsTable.module.css';
const messages = defineMessages({
more: { id: 'label.more', defaultMessage: 'More' },
});
export default function MetricsTable({ export default function MetricsTable({
websiteId, websiteId,
type, type,
@ -35,7 +31,7 @@ export default function MetricsTable({
router, router,
query: { url, referrer, os, browser, device, country }, query: { url, referrer, os, browser, device, country },
} = usePageQuery(); } = usePageQuery();
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, isFetched, error } = useQuery( const { data, isLoading, isFetched, error } = useQuery(
@ -81,7 +77,7 @@ export default function MetricsTable({
{data && !error && limit && ( {data && !error && limit && (
<Link href={router.pathname} as={resolveUrl({ view: type })}> <Link href={router.pathname} as={resolveUrl({ view: type })}>
<Button variant="quiet"> <Button variant="quiet">
<Text>{formatMessage(messages.more)}</Text> <Text>{formatMessage(labels.more)}</Text>
<Icon size="sm"> <Icon size="sm">
<Icons.ArrowRight /> <Icons.ArrowRight />
</Icon> </Icon>

View File

@ -1,10 +1,9 @@
import { useIntl } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function OSTable({ websiteId, ...props }) { export default function OSTable({ websiteId, ...props }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
function renderLink({ x: os }) { function renderLink({ x: os }) {
return <FilterLink id="os" value={os} />; return <FilterLink id="os" value={os} />;

View File

@ -1,11 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import FilterButtons from 'components/common/FilterButtons'; import FilterButtons from 'components/common/FilterButtons';
import { urlFilter } from 'lib/filters'; import { urlFilter } from 'lib/filters';
import { labels } from 'components/messages';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants'; import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import useMessages from 'hooks/useMessages';
const filters = { const filters = {
[FILTER_RAW]: null, [FILTER_RAW]: null,
@ -14,7 +13,7 @@ const filters = {
export default function PagesTable({ websiteId, showFilters, ...props }) { export default function PagesTable({ websiteId, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const buttons = [ const buttons = [
{ {

View File

@ -1,11 +1,10 @@
import { useMemo } from 'react';
import { useVisible } from 'react-basics'; import { useVisible } from 'react-basics';
import { useIntl } from 'react-intl';
import { colord } from 'colord'; import { colord } from 'colord';
import BarChart from './BarChart'; import BarChart from './BarChart';
import useTheme from 'hooks/useTheme';
import { THEME_COLORS, DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import { THEME_COLORS, DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { labels } from 'components/messages'; import useTheme from 'hooks/useTheme';
import { useMemo } from 'react'; import useMessages from 'hooks/useMessages';
export default function PageviewsChart({ export default function PageviewsChart({
websiteId, websiteId,
@ -17,7 +16,7 @@ export default function PageviewsChart({
animationDuration = DEFAULT_ANIMATION_DURATION, animationDuration = DEFAULT_ANIMATION_DURATION,
...props ...props
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const [theme] = useTheme(); const [theme] = useTheme();
const { ref, visible } = useVisible(); const { ref, visible } = useVisible();

View File

@ -1,12 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl';
import { safeDecodeURI } from 'next-basics'; import { safeDecodeURI } from 'next-basics';
import Tag from 'components/common/Tag'; import Tag from 'components/common/Tag';
import FilterButtons from 'components/common/FilterButtons'; import FilterButtons from 'components/common/FilterButtons';
import { paramFilter } from 'lib/filters'; import { paramFilter } from 'lib/filters';
import { FILTER_RAW, FILTER_COMBINED } from 'lib/constants'; import { FILTER_RAW, FILTER_COMBINED } from 'lib/constants';
import { labels } from 'components/messages';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import useMessages from 'hooks/useMessages';
const filters = { const filters = {
[FILTER_RAW]: null, [FILTER_RAW]: null,
@ -15,7 +14,7 @@ const filters = {
export default function QueryParametersTable({ websiteId, showFilters, ...props }) { export default function QueryParametersTable({ websiteId, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const buttons = [ const buttons = [
{ {

View File

@ -1,10 +1,9 @@
import { useIntl } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function ReferrersTable({ websiteId, ...props }) { export default function ReferrersTable({ websiteId, ...props }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const renderLink = ({ w: link, x: referrer }) => { const renderLink = ({ w: link, x: referrer }) => {
return referrer ? ( return referrer ? (

View File

@ -1,12 +1,11 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Button, Icon, Text, Row, Column } from 'react-basics'; import { Button, Icon, Text, Row, Column } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart'; import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar'; import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader'; import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/input/DateFilter'; import DateFilter from 'components/input/DateFilter';
import StickyHeader from 'components/common/StickyHeader';
import ErrorMessage from 'components/common/ErrorMessage'; import ErrorMessage from 'components/common/ErrorMessage';
import FilterTags from 'components/metrics/FilterTags'; import FilterTags from 'components/metrics/FilterTags';
import RefreshButton from 'components/input/RefreshButton'; import RefreshButton from 'components/input/RefreshButton';
@ -16,9 +15,9 @@ import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength } from 'lib/date'; import { getDateArray, getDateLength } from 'lib/date';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { UI_LAYOUT_BODY } from 'lib/constants';
import { labels } from 'components/messages';
import styles from './WebsiteChart.module.css'; import styles from './WebsiteChart.module.css';
import useSticky from 'hooks/useSticky';
import useMessages from 'hooks/useMessages';
export default function WebsiteChart({ export default function WebsiteChart({
websiteId, websiteId,
@ -29,7 +28,7 @@ export default function WebsiteChart({
showDetailsButton = false, showDetailsButton = false,
onDataLoad = () => {}, onDataLoad = () => {},
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange; const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone(); const [timezone] = useTimezone();
@ -37,6 +36,7 @@ export default function WebsiteChart({
query: { url, referrer, os, browser, device, country }, query: { url, referrer, os, browser, device, country },
} = usePageQuery(); } = usePageQuery();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { ref, isSticky } = useSticky({ enabled: stickyHeader });
const { data, isLoading, error } = useQuery( const { data, isLoading, error } = useQuery(
['websites:pageviews', websiteId, modified, url, referrer, os, browser, device, country], ['websites:pageviews', websiteId, modified, url, referrer, os, browser, device, country],
@ -81,12 +81,13 @@ export default function WebsiteChart({
)} )}
</WebsiteHeader> </WebsiteHeader>
<FilterTags websiteId={websiteId} params={{ url, referrer, os, browser, device, country }} /> <FilterTags websiteId={websiteId} params={{ url, referrer, os, browser, device, country }} />
<StickyHeader <Row
stickyClassName={styles.sticky} ref={ref}
enabled={stickyHeader} className={classNames(styles.header, {
scrollElement={document.getElementById(UI_LAYOUT_BODY) || document} [styles.sticky]: stickyHeader,
[styles.isSticky]: isSticky,
})}
> >
<Row className={styles.header}>
<Column> <Column>
<MetricsBar websiteId={websiteId} /> <MetricsBar websiteId={websiteId} />
</Column> </Column>
@ -95,7 +96,6 @@ export default function WebsiteChart({
<DateFilter websiteId={websiteId} value={value} className={styles.dropdown} /> <DateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
</Column> </Column>
</Row> </Row>
</StickyHeader>
<Row> <Row>
<Column className={styles.chart}> <Column className={styles.chart}>
{error && <ErrorMessage />} {error && <ErrorMessage />}

View File

@ -17,22 +17,23 @@
} }
.header { .header {
position: relative;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 10px 0;
min-height: 90px; min-height: 90px;
margin-bottom: 20px; margin-bottom: 20px;
background: var(--base50);
} }
.sticky { .sticky {
position: fixed; position: sticky;
top: 0; top: -1px;
background: var(--base50); z-index: 2;
}
.isSticky {
border-bottom: 1px solid var(--base300); border-bottom: 1px solid var(--base300);
z-index: 3;
width: inherit;
padding-top: 10px;
} }
.actions { .actions {

View File

@ -1,6 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Icon, Icons, Text, Flexbox } from 'react-basics'; import { Button, Icon, Icons, Text, Flexbox } from 'react-basics';
import { useIntl } from 'react-intl';
import Link from 'next/link'; import Link from 'next/link';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
@ -9,16 +8,16 @@ import DashboardSettingsButton from 'components/pages/dashboard/DashboardSetting
import DashboardEdit from 'components/pages/dashboard/DashboardEdit'; import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages';
import useDashboard from 'store/dashboard'; import useDashboard from 'store/dashboard';
import useMessages from 'hooks/useMessages';
export default function Dashboard({ userId }) { export default function Dashboard({ userId }) {
const { formatMessage, labels, messages } = useMessages();
const dashboard = useDashboard(); const dashboard = useDashboard();
const { showCharts, limit, editing } = dashboard; const { showCharts, limit, editing } = dashboard;
const [max, setMax] = useState(limit); const [max, setMax] = useState(limit);
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites'], () => get('/websites', { userId })); const { data, isLoading, error } = useQuery(['websites'], () => get('/websites', { userId }));
const { formatMessage } = useIntl();
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;
function handleMore() { function handleMore() {

View File

@ -1,24 +1,18 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames'; import classNames from 'classnames';
import { Button } from 'react-basics'; import { Button } from 'react-basics';
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import useDashboard, { saveDashboard } from 'store/dashboard'; import useDashboard, { saveDashboard } from 'store/dashboard';
import useMessages from 'hooks/useMessages';
import styles from './DashboardEdit.module.css'; import styles from './DashboardEdit.module.css';
const messages = defineMessages({
save: { id: 'label.save', defaultMessage: 'Save' },
reset: { id: 'label.reset', defaultMessage: 'Reset' },
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
});
const dragId = 'dashboard-website-ordering'; const dragId = 'dashboard-website-ordering';
export default function DashboardEdit({ websites }) { export default function DashboardEdit({ websites }) {
const settings = useDashboard(); const settings = useDashboard();
const { websiteOrder } = settings; const { websiteOrder } = settings;
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const [order, setOrder] = useState(websiteOrder || []); const [order, setOrder] = useState(websiteOrder || []);
const ordered = useMemo( const ordered = useMemo(
@ -58,13 +52,13 @@ export default function DashboardEdit({ websites }) {
<> <>
<div className={styles.buttons}> <div className={styles.buttons}>
<Button onClick={handleSave} variant="action" size="small"> <Button onClick={handleSave} variant="action" size="small">
{formatMessage(messages.save)} {formatMessage(labels.save)}
</Button> </Button>
<Button onClick={handleCancel} size="small"> <Button onClick={handleCancel} size="small">
{formatMessage(messages.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
<Button onClick={handleReset} size="small"> <Button onClick={handleReset} size="small">
{formatMessage(messages.reset)} {formatMessage(labels.reset)}
</Button> </Button>
</div> </div>
<div className={styles.dragActive}> <div className={styles.dragActive}>

View File

@ -1,24 +1,18 @@
import { defineMessages, useIntl } from 'react-intl';
import { Menu, Icon, Text, PopupTrigger, Popup, Item, Button } from 'react-basics'; import { Menu, Icon, Text, PopupTrigger, Popup, Item, Button } from 'react-basics';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages';
import { saveDashboard } from 'store/dashboard'; import { saveDashboard } from 'store/dashboard';
import useMessages from 'hooks/useMessages';
const messages = defineMessages({
toggleCharts: { id: 'message.toggle-charts', defaultMessage: 'Toggle charts' },
editDashboard: { id: 'message.edit-dashboard', defaultMessage: 'Edit dashboard' },
});
export default function DashboardSettingsButton() { export default function DashboardSettingsButton() {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const menuOptions = [ const menuOptions = [
{ {
label: formatMessage(messages.toggleCharts), label: formatMessage(labels.toggleCharts),
value: 'charts', value: 'charts',
}, },
{ {
label: formatMessage(messages.editDashboard), label: formatMessage(labels.editDashboard),
value: 'order', value: 'order',
}, },
]; ];

View File

@ -1,12 +1,11 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useIntl } from 'react-intl';
import { labels } from 'components/messages';
import DataTable from 'components/metrics/DataTable'; import DataTable from 'components/metrics/DataTable';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import useMessages from 'hooks/useMessages';
export default function RealtimeCountries({ data }) { export default function RealtimeCountries({ data }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { locale } = useLocale(); const { locale } = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);

View File

@ -1,12 +1,10 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { subMinutes, startOfMinute } from 'date-fns'; import { subMinutes, startOfMinute } from 'date-fns';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import firstBy from 'thenby'; import firstBy from 'thenby';
import { GridRow, GridColumn } from 'components/layout/Grid'; import { GridRow, GridColumn } from 'components/layout/Grid';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import RealtimeChart from 'components/metrics/RealtimeChart'; import RealtimeChart from 'components/metrics/RealtimeChart';
import StickyHeader from 'components/common/StickyHeader';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import WorldMap from 'components/common/WorldMap'; import WorldMap from 'components/common/WorldMap';
import RealtimeLog from 'components/pages/realtime/RealtimeLog'; import RealtimeLog from 'components/pages/realtime/RealtimeLog';
@ -15,8 +13,8 @@ import RealtimeUrls from 'components/pages/realtime/RealtimeUrls';
import RealtimeCountries from 'components/pages/realtime/RealtimeCountries'; import RealtimeCountries from 'components/pages/realtime/RealtimeCountries';
import WebsiteSelect from 'components/input/WebsiteSelect'; import WebsiteSelect from 'components/input/WebsiteSelect';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import { labels } from 'components/messages';
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
import styles from './RealtimeDashboard.module.css'; import styles from './RealtimeDashboard.module.css';
@ -28,7 +26,7 @@ function mergeData(state = [], data = [], time) {
} }
export default function RealtimeDashboard({ websiteId }) { export default function RealtimeDashboard({ websiteId }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const router = useRouter(); const router = useRouter();
const [currentData, setCurrentData] = useState(); const [currentData, setCurrentData] = useState();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
@ -104,9 +102,7 @@ export default function RealtimeDashboard({ websiteId }) {
<PageHeader title={formatMessage(labels.realtime)}> <PageHeader title={formatMessage(labels.realtime)}>
<WebsiteSelect websiteId={websiteId} onSelect={handleSelect} /> <WebsiteSelect websiteId={websiteId} onSelect={handleSelect} />
</PageHeader> </PageHeader>
<StickyHeader stickyClassName={styles.sticky}>
<RealtimeHeader websiteId={websiteId} data={currentData} /> <RealtimeHeader websiteId={websiteId} data={currentData} />
</StickyHeader>
<div className={styles.chart}> <div className={styles.chart}>
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} /> <RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
</div> </div>

View File

@ -1,10 +1,9 @@
import { useIntl } from 'react-intl';
import MetricCard from 'components/metrics/MetricCard'; import MetricCard from 'components/metrics/MetricCard';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
import styles from './RealtimeHeader.module.css'; import styles from './RealtimeHeader.module.css';
export default function RealtimeHeader({ data = {} }) { export default function RealtimeHeader({ data = {} }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { pageviews, visitors, events, countries } = data; const { pageviews, visitors, events, countries } = data;
return ( return (

View File

@ -1,14 +1,13 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useIntl } from 'react-intl';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useMessages from 'hooks/useMessages';
export default function RealtimeHome() { export default function RealtimeHome() {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const router = useRouter(); const router = useRouter();
const { data, isLoading, error } = useQuery(['websites:me'], () => get('/me/websites')); const { data, isLoading, error } = useQuery(['websites:me'], () => get('/me/websites'));

View File

@ -1,11 +1,9 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { StatusLight, Icon, Text } from 'react-basics'; import { StatusLight, Icon, Text } from 'react-basics';
import { useIntl, FormattedMessage } from 'react-intl';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import firstBy from 'thenby'; import firstBy from 'thenby';
import FilterButtons from 'components/common/FilterButtons'; import FilterButtons from 'components/common/FilterButtons';
import NoData from 'components/common/NoData'; import NoData from 'components/common/NoData';
import { labels, messages } from 'components/messages';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import { BROWSERS } from 'lib/constants'; import { BROWSERS } from 'lib/constants';
@ -14,6 +12,7 @@ import { dateFormat } from 'lib/date';
import { safeDecodeURI } from 'next-basics'; import { safeDecodeURI } from 'next-basics';
import Icons from 'components/icons'; import Icons from 'components/icons';
import styles from './RealtimeLog.module.css'; import styles from './RealtimeLog.module.css';
import useMessages from 'hooks/useMessages';
const TYPE_ALL = 'all'; const TYPE_ALL = 'all';
const TYPE_PAGEVIEW = 'pageview'; const TYPE_PAGEVIEW = 'pageview';
@ -27,7 +26,7 @@ const icons = {
}; };
export default function RealtimeLog({ data, websiteDomain }) { export default function RealtimeLog({ data, websiteDomain }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { locale } = useLocale(); const { locale } = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
const [filter, setFilter] = useState(TYPE_ALL); const [filter, setFilter] = useState(TYPE_ALL);

View File

@ -1,14 +1,13 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { ButtonGroup, Button, Flexbox } from 'react-basics'; import { ButtonGroup, Button, Flexbox } from 'react-basics';
import { useIntl } from 'react-intl';
import firstBy from 'thenby'; import firstBy from 'thenby';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import DataTable from 'components/metrics/DataTable'; import DataTable from 'components/metrics/DataTable';
import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function RealtimeUrls({ websiteDomain, data = {} }) { export default function RealtimeUrls({ websiteDomain, data = {} }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { pageviews } = data; const { pageviews } = data;
const [filter, setFilter] = useState(FILTER_REFERRERS); const [filter, setFilter] = useState(FILTER_REFERRERS);

View File

@ -1,12 +1,11 @@
import { useIntl } from 'react-intl';
import DateFilter from 'components/input/DateFilter'; import DateFilter from 'components/input/DateFilter';
import { Button, Flexbox } from 'react-basics'; import { Button, Flexbox } from 'react-basics';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import { DEFAULT_DATE_RANGE } from 'lib/constants'; import { DEFAULT_DATE_RANGE } from 'lib/constants';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function DateRangeSetting() { export default function DateRangeSetting() {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const [dateRange, setDateRange] = useDateRange(); const [dateRange, setDateRange] = useDateRange();
const { startDate, endDate, value } = dateRange; const { startDate, endDate, value } = dateRange;

View File

@ -1,12 +1,11 @@
import { useIntl } from 'react-intl';
import { Button, Dropdown, Item, Flexbox } from 'react-basics'; import { Button, Dropdown, Item, Flexbox } from 'react-basics';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import { DEFAULT_LOCALE } from 'lib/constants'; import { DEFAULT_LOCALE } from 'lib/constants';
import { languages } from 'lib/lang'; import { languages } from 'lib/lang';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function LanguageSetting() { export default function LanguageSetting() {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { locale, saveLocale } = useLocale(); const { locale, saveLocale } = useLocale();
const options = Object.keys(languages); const options = Object.keys(languages);

View File

@ -1,11 +1,10 @@
import { useIntl } from 'react-intl';
import { Button, Icon, Text, useToast, ModalTrigger, Modal } from 'react-basics'; import { Button, Icon, Text, useToast, ModalTrigger, Modal } from 'react-basics';
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm'; import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function PasswordChangeButton() { export default function PasswordChangeButton() {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { toast, showToast } = useToast(); const { toast, showToast } = useToast();
const handleSave = () => { const handleSave = () => {

View File

@ -1,11 +1,10 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics'; import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function PasswordEditForm({ onSave, onClose }) { export default function PasswordEditForm({ onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/me/password', data)); const { mutate, error, isLoading } = useMutation(data => post('/me/password', data));
const ref = useRef(null); const ref = useRef(null);

View File

@ -1,15 +1,14 @@
import { Form, FormRow } from 'react-basics'; import { Form, FormRow } from 'react-basics';
import { useIntl } from 'react-intl';
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting'; import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting'; import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting'; import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
import ThemeSetting from 'components/pages/settings/profile/ThemeSetting'; import ThemeSetting from 'components/pages/settings/profile/ThemeSetting';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function ProfileDetails() { export default function ProfileDetails() {
const { user } = useUser(); const { user } = useUser();
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
if (!user) { if (!user) {
return null; return null;
@ -20,7 +19,9 @@ export default function ProfileDetails() {
return ( return (
<Form> <Form>
<FormRow label={formatMessage(labels.username)}>{username}</FormRow> <FormRow label={formatMessage(labels.username)}>{username}</FormRow>
<FormRow label={formatMessage(labels.role)}>{role}</FormRow> <FormRow label={formatMessage(labels.role)}>
{formatMessage(labels[role] || labels.unknown)}
</FormRow>
<FormRow label={formatMessage(labels.defaultDateRange)}> <FormRow label={formatMessage(labels.defaultDateRange)}>
<DateRangeSetting /> <DateRangeSetting />
</FormRow> </FormRow>

View File

@ -1,13 +1,12 @@
import { useIntl } from 'react-intl';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import ProfileDetails from './ProfileDetails'; import ProfileDetails from './ProfileDetails';
import PasswordChangeButton from './PasswordChangeButton'; import PasswordChangeButton from './PasswordChangeButton';
import { labels } from 'components/messages';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
import useMessages from 'hooks/useMessages';
export default function ProfileSettings() { export default function ProfileSettings() {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { cloudMode } = useConfig(); const { cloudMode } = useConfig();
return ( return (

View File

@ -1,12 +1,11 @@
import { Dropdown, Item, Button, Flexbox } from 'react-basics'; import { Dropdown, Item, Button, Flexbox } from 'react-basics';
import { useIntl } from 'react-intl';
import { listTimeZones } from 'timezone-support'; import { listTimeZones } from 'timezone-support';
import useTimezone from 'hooks/useTimezone'; import useTimezone from 'hooks/useTimezone';
import useMessages from 'hooks/useMessages';
import { getTimezone } from 'lib/date'; import { getTimezone } from 'lib/date';
import { labels } from 'components/messages';
export default function TimezoneSetting() { export default function TimezoneSetting() {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const [timezone, saveTimezone] = useTimezone(); const [timezone, saveTimezone] = useTimezone();
const options = listTimeZones(); const options = listTimeZones();

View File

@ -1,5 +1,4 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { useIntl } from 'react-intl';
import { import {
Form, Form,
FormRow, FormRow,
@ -10,10 +9,10 @@ import {
SubmitButton, SubmitButton,
} from 'react-basics'; } from 'react-basics';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, getMessage } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function TeamJoinForm({ onSave, onClose }) { export default function TeamJoinForm({ onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, getMessage } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post('/teams/join', data)); const { mutate, error } = useMutation(data => post('/teams/join', data));
const ref = useRef(null); const ref = useRef(null);
@ -28,7 +27,7 @@ export default function TeamJoinForm({ onSave, onClose }) {
}; };
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error && getMessage(error, formatMessage)}> <Form ref={ref} onSubmit={handleSubmit} error={error && getMessage(error)}>
<FormRow label={formatMessage(labels.accessCode)}> <FormRow label={formatMessage(labels.accessCode)}>
<FormInput name="accessCode" rules={{ required: formatMessage(labels.required) }}> <FormInput name="accessCode" rules={{ required: formatMessage(labels.required) }}>
<TextField autoComplete="off" /> <TextField autoComplete="off" />

View File

@ -1,5 +1,4 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { useIntl } from 'react-intl';
import { import {
Form, Form,
FormRow, FormRow,
@ -10,10 +9,10 @@ import {
SubmitButton, SubmitButton,
} from 'react-basics'; } from 'react-basics';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function TeamAddForm({ onSave, onClose }) { export default function TeamAddForm({ onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/teams', data)); const { mutate, error, isLoading } = useMutation(data => post('/teams', data));
const ref = useRef(null); const ref = useRef(null);

View File

@ -1,10 +1,9 @@
import { Button, Form, FormButtons, SubmitButton } from 'react-basics'; import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import { useIntl, FormattedMessage } from 'react-intl';
import { labels, messages } from 'components/messages';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
export default function TeamDeleteForm({ teamId, teamName, onSave, onClose }) { export default function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => del(`/teams/${teamId}`, data)); const { mutate, error, isLoading } = useMutation(data => del(`/teams/${teamId}`, data));

View File

@ -8,16 +8,15 @@ import {
Button, Button,
Flexbox, Flexbox,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import { getRandomChars } from 'next-basics'; import { getRandomChars } from 'next-basics';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
const generateId = () => getRandomChars(16); const generateId = () => getRandomChars(16);
export default function TeamEditForm({ teamId, data, onSave, readOnly }) { export default function TeamEditForm({ teamId, data, onSave, readOnly }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data)); const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data));
const ref = useRef(null); const ref = useRef(null);

View File

@ -1,13 +1,12 @@
import { messages } from 'components/messages'; import { Loading, useToast } from 'react-basics';
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable'; import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { Loading, useToast } from 'react-basics'; import useMessages from 'hooks/useMessages';
import { useIntl } from 'react-intl';
export default function TeamMembers({ teamId, readOnly }) { export default function TeamMembers({ teamId, readOnly }) {
const { toast, showToast } = useToast(); const { toast, showToast } = useToast();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () => const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () =>
get(`/teams/${teamId}/users`), get(`/teams/${teamId}/users`),
); );
@ -18,7 +17,7 @@ export default function TeamMembers({ teamId, readOnly }) {
const handleSave = async () => { const handleSave = async () => {
await refetch(); await refetch();
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(labels.saved), variant: 'success' });
}; };
return ( return (

View File

@ -11,14 +11,13 @@ import {
Flexbox, Flexbox,
Text, Text,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { labels } from 'components/messages';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
export default function TeamMembersTable({ data = [], onSave, readOnly }) { export default function TeamMembersTable({ data = [], onSave, readOnly }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate } = useMutation(data => del(`/teamUsers/${data.teamUserId}`)); const { mutate } = useMutation(data => del(`/teamUsers/${data.teamUserId}`));

View File

@ -1,19 +1,18 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics'; import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import { labels, messages } from 'components/messages';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
import TeamEditForm from './TeamEditForm'; import TeamEditForm from './TeamEditForm';
import TeamMembers from './TeamMembers'; import TeamMembers from './TeamMembers';
import TeamWebsites from './TeamWebsites'; import TeamWebsites from './TeamWebsites';
export default function TeamSettings({ teamId }) { export default function TeamSettings({ teamId }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { user } = useUser(); const { user } = useUser();
const [values, setValues] = useState(null); const [values, setValues] = useState(null);
const [tab, setTab] = useState('details'); const [tab, setTab] = useState('details');

View File

@ -1,7 +1,3 @@
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import { labels, messages } from 'components/messages';
import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable';
import useApi from 'hooks/useApi';
import { import {
ActionForm, ActionForm,
Button, Button,
@ -13,12 +9,15 @@ import {
Text, Text,
useToast, useToast,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable';
import WebsiteAddTeamForm from 'components/pages/settings/teams/WebsiteAddTeamForm'; import WebsiteAddTeamForm from 'components/pages/settings/teams/WebsiteAddTeamForm';
import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
export default function TeamWebsites({ teamId }) { export default function TeamWebsites({ teamId }) {
const { toast, showToast } = useToast(); const { toast, showToast } = useToast();
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () => const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () =>
get(`/teams/${teamId}/websites`), get(`/teams/${teamId}/websites`),

View File

@ -12,13 +12,12 @@ import {
Icons, Icons,
Flexbox, Flexbox,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import { labels } from 'components/messages';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
export default function TeamWebsitesTable({ data = [], onSave }) { export default function TeamWebsitesTable({ data = [], onSave }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate } = useMutation(({ teamWebsiteId }) => del(`/teamWebsites/${teamWebsiteId}`)); const { mutate } = useMutation(({ teamWebsiteId }) => del(`/teamWebsites/${teamWebsiteId}`));

View File

@ -1,18 +1,17 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Icon, Modal, ModalTrigger, useToast, Text, Flexbox } from 'react-basics'; import { Button, Icon, Modal, ModalTrigger, useToast, Text, Flexbox } from 'react-basics';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import TeamsTable from 'components/pages/settings/teams/TeamsTable'; import TeamsTable from 'components/pages/settings/teams/TeamsTable';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import { labels, messages } from 'components/messages';
import Icons from 'components/icons'; import Icons from 'components/icons';
import TeamJoinForm from './JoinTeamForm'; import TeamJoinForm from './JoinTeamForm';
import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
export default function TeamsList() { export default function TeamsList() {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const [update, setUpdate] = useState(0); const [update, setUpdate] = useState(0);
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`)); const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`));

View File

@ -1,6 +1,3 @@
import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
import { ROLES } from 'lib/constants';
import Link from 'next/link'; import Link from 'next/link';
import { import {
Button, Button,
@ -17,11 +14,13 @@ import {
TableRow, TableRow,
Text, Text,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import TeamDeleteForm from './TeamDeleteForm'; import TeamDeleteForm from './TeamDeleteForm';
import useMessages from 'hooks/useMessages';
import useUser from 'hooks/useUser';
import { ROLES } from 'lib/constants';
export default function TeamsTable({ data = [], onDelete }) { export default function TeamsTable({ data = [], onDelete }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
const columns = [ const columns = [

View File

@ -1,12 +1,11 @@
import { labels } from 'components/messages';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from 'react-basics'; import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from 'react-basics';
import { useIntl } from 'react-intl';
import WebsiteTags from './WebsiteTags'; import WebsiteTags from './WebsiteTags';
import useMessages from 'hooks/useMessages';
export default function WebsiteAddTeamForm({ teamId, onSave, onClose }) { export default function WebsiteAddTeamForm({ teamId, onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { get, post, useQuery, useMutation } = useApi(); const { get, post, useQuery, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data)); const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data));
const { data: websites } = useQuery(['websites'], () => get('/websites')); const { data: websites } = useQuery(['websites'], () => get('/websites'));

View File

@ -1,10 +1,9 @@
import { useIntl } from 'react-intl';
import { Button, Icon, Text, Modal, Icons, ModalTrigger } from 'react-basics'; import { Button, Icon, Text, Modal, Icons, ModalTrigger } from 'react-basics';
import UserAddForm from './UserAddForm'; import UserAddForm from './UserAddForm';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function UserAddButton({ onSave }) { export default function UserAddButton({ onSave }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const handleSave = () => { const handleSave = () => {
onSave(); onSave();

View File

@ -10,15 +10,14 @@ import {
SubmitButton, SubmitButton,
Button, Button,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function UserAddForm({ onSave, onClose }) { export default function UserAddForm({ onSave, onClose }) {
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post(`/users`, data)); const { mutate, error, isLoading } = useMutation(data => post(`/users`, data));
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const handleSubmit = async data => { const handleSubmit = async data => {
mutate(data, { mutate(data, {

View File

@ -1,11 +1,10 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import useApi from 'hooks/useApi';
import { Button, Form, FormButtons, SubmitButton } from 'react-basics'; import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import { useIntl, FormattedMessage } from 'react-intl'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function UserDeleteForm({ userId, username, onSave, onClose }) { export default function UserDeleteForm({ userId, username, onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, FormattedMessage, labels, messages } = useMessages();
const { del } = useApi(); const { del } = useApi();
const { mutate, error, isLoading } = useMutation(() => del(`/users/${userId}`)); const { mutate, error, isLoading } = useMutation(() => del(`/users/${userId}`));

View File

@ -9,13 +9,12 @@ import {
SubmitButton, SubmitButton,
PasswordField, PasswordField,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function UserEditForm({ userId, data, onSave }) { export default function UserEditForm({ userId, data, onSave }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(({ username }) => post(`/users/${userId}`, { username })); const { mutate, error } = useMutation(({ username }) => post(`/users/${userId}`, { username }));

View File

@ -1,16 +1,15 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics'; import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import UserEditForm from 'components/pages/settings/users//UserEditForm'; import UserEditForm from 'components/pages/settings/users//UserEditForm';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages';
import UserWebsites from './UserWebsites'; import UserWebsites from './UserWebsites';
import useMessages from 'hooks/useMessages';
export default function UserSettings({ userId }) { export default function UserSettings({ userId }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
const [values, setValues] = useState(null); const [values, setValues] = useState(null);
const [tab, setTab] = useState('details'); const [tab, setTab] = useState('details');

View File

@ -1,11 +1,10 @@
import { Loading } from 'react-basics'; import { Loading } from 'react-basics';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
import { messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function UserWebsites({ userId }) { export default function UserWebsites({ userId }) {
const { formatMessage } = useIntl(); const { formatMessage, messages } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['user:websites', userId], () => const { data, isLoading } = useQuery(['user:websites', userId], () =>
get(`/users/${userId}/websites`), get(`/users/${userId}/websites`),

View File

@ -1,4 +1,4 @@
import { useIntl } from 'react-intl'; import { useToast } from 'react-basics';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
@ -6,11 +6,10 @@ import UsersTable from './UsersTable';
import UserAddButton from './UserAddButton'; import UserAddButton from './UserAddButton';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { useToast } from 'react-basics'; import useMessages from 'hooks/useMessages';
import { labels, messages } from 'components/messages';
export default function UsersList() { export default function UsersList() {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { user } = useUser(); const { user } = useUser();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), { const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), {

View File

@ -13,16 +13,15 @@ import {
ModalTrigger, ModalTrigger,
Modal, Modal,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import Link from 'next/link'; import Link from 'next/link';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import UserDeleteForm from './UserDeleteForm'; import UserDeleteForm from './UserDeleteForm';
import { labels } from 'components/messages';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import useMessages from 'hooks/useMessages';
export default function UsersTable({ data = [], onDelete }) { export default function UsersTable({ data = [], onDelete }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
const columns = [ const columns = [

View File

@ -8,16 +8,15 @@ import {
Button, Button,
Toggle, Toggle,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { getRandomChars } from 'next-basics'; import { getRandomChars } from 'next-basics';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
const generateId = () => getRandomChars(16); const generateId = () => getRandomChars(16);
export default function ShareUrl({ websiteId, data, onSave }) { export default function ShareUrl({ websiteId, data, onSave }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { name, shareId } = data; const { name, shareId } = data;
const [id, setId] = useState(shareId); const [id, setId] = useState(shareId);
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();

View File

@ -1,10 +1,9 @@
import { TextArea } from 'react-basics'; import { TextArea } from 'react-basics';
import { TRACKER_SCRIPT_URL } from 'lib/constants'; import { TRACKER_SCRIPT_URL } from 'lib/constants';
import { messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
import { useIntl } from 'react-intl';
export default function TrackingCode({ websiteId }) { export default function TrackingCode({ websiteId }) {
const { formatMessage } = useIntl(); const { formatMessage, messages } = useMessages();
const url = TRACKER_SCRIPT_URL.startsWith('http') const url = TRACKER_SCRIPT_URL.startsWith('http')
? TRACKER_SCRIPT_URL ? TRACKER_SCRIPT_URL
: `${location.origin}${TRACKER_SCRIPT_URL}`; : `${location.origin}${TRACKER_SCRIPT_URL}`;

View File

@ -7,17 +7,12 @@ import {
Button, Button,
SubmitButton, SubmitButton,
} from 'react-basics'; } from 'react-basics';
import { defineMessages, useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
const messages = defineMessages({
invalidDomain: { id: 'label.invalid-domain', defaultMessage: 'Invalid domain' },
});
export default function WebsiteAddForm({ onSave, onClose }) { export default function WebsiteAddForm({ onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/websites', data)); const { mutate, error, isLoading } = useMutation(data => post('/websites', data));
@ -42,7 +37,7 @@ export default function WebsiteAddForm({ onSave, onClose }) {
name="domain" name="domain"
rules={{ rules={{
required: formatMessage(labels.required), required: formatMessage(labels.required),
pattern: { value: DOMAIN_REGEX, message: formatMessage(messages.invalidDomain) }, pattern: { value: DOMAIN_REGEX, message: formatMessage(labels.invalidDomain) },
}} }}
> >
<TextField autoComplete="off" /> <TextField autoComplete="off" />

View File

@ -1,11 +1,10 @@
import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics';
import { useIntl } from 'react-intl';
import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm'; import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm';
import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm'; import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function WebsiteData({ websiteId, onSave }) { export default function WebsiteData({ websiteId, onSave }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const handleReset = async () => { const handleReset = async () => {
onSave('reset'); onSave('reset');

View File

@ -7,14 +7,13 @@ import {
SubmitButton, SubmitButton,
TextField, TextField,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import { labels, messages } from 'components/messages';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
const CONFIRM_VALUE = 'DELETE'; const CONFIRM_VALUE = 'DELETE';
export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) { export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, error } = useMutation(data => del(`/websites/${websiteId}`, data)); const { mutate, error } = useMutation(data => del(`/websites/${websiteId}`, data));

View File

@ -1,12 +1,11 @@
import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics'; import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
import { useRef } from 'react'; import { useRef } from 'react';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function WebsiteEditForm({ websiteId, data, onSave }) { export default function WebsiteEditForm({ websiteId, data, onSave }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/websites/${websiteId}`, data)); const { mutate, error } = useMutation(data => post(`/websites/${websiteId}`, data));
const ref = useRef(null); const ref = useRef(null);

View File

@ -8,13 +8,12 @@ import {
TextField, TextField,
} from 'react-basics'; } from 'react-basics';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { useIntl } from 'react-intl'; import useMessages from 'hooks/useMessages';
import { labels, messages } from 'components/messages';
const CONFIRM_VALUE = 'RESET'; const CONFIRM_VALUE = 'RESET';
export default function WebsiteResetForm({ websiteId, onSave, onClose }) { export default function WebsiteResetForm({ websiteId, onSave, onClose }) {
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/websites/${websiteId}/reset`, data)); const { mutate, error } = useMutation(data => post(`/websites/${websiteId}/reset`, data));

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Breadcrumbs, Item, Tabs, useToast, Button, Text, Icon, Icons } from 'react-basics'; import { Breadcrumbs, Item, Tabs, useToast, Button, Text, Icon, Icons } from 'react-basics';
import { useIntl } from 'react-intl';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
@ -10,11 +9,11 @@ import WebsiteData from 'components/pages/settings/websites/WebsiteData';
import TrackingCode from 'components/pages/settings/websites/TrackingCode'; import TrackingCode from 'components/pages/settings/websites/TrackingCode';
import ShareUrl from 'components/pages/settings/websites/ShareUrl'; import ShareUrl from 'components/pages/settings/websites/ShareUrl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function WebsiteSettings({ websiteId }) { export default function WebsiteSettings({ websiteId }) {
const router = useRouter(); const router = useRouter();
const { formatMessage } = useIntl(); const { formatMessage, labels, messages } = useMessages();
const [values, setValues] = useState(null); const [values, setValues] = useState(null);
const [tab, setTab] = useState('details'); const [tab, setTab] = useState('details');
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();

View File

@ -1,5 +1,4 @@
import { Button, Icon, Text, Modal, ModalTrigger, useToast, Icons } from 'react-basics'; import { Button, Icon, Text, Modal, ModalTrigger, useToast, Icons } from 'react-basics';
import { useIntl } from 'react-intl';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
@ -7,9 +6,10 @@ import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { labels, messages } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function WebsitesList() { export default function WebsitesList() {
const { formatMessage, labels, messages } = useMessages();
const { user } = useUser(); const { user } = useUser();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery( const { data, isLoading, error, refetch } = useQuery(
@ -18,7 +18,6 @@ export default function WebsitesList() {
{ enabled: !!user }, { enabled: !!user },
); );
const { toast, showToast } = useToast(); const { toast, showToast } = useToast();
const { formatMessage } = useIntl();
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;
const handleSave = async () => { const handleSave = async () => {

View File

@ -12,11 +12,10 @@ import {
Icons, Icons,
Flexbox, Flexbox,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl'; import useMessages from 'hooks/useMessages';
import { labels } from 'components/messages';
export default function WebsitesTable({ data = [] }) { export default function WebsitesTable({ data = [] }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const columns = [ const columns = [
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } }, { name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },

View File

@ -1,5 +1,4 @@
import { Menu, Item, Icon, Button, Flexbox, Text } from 'react-basics'; import { Menu, Item, Icon, Button, Flexbox, Text } from 'react-basics';
import { useIntl } from 'react-intl';
import Link from 'next/link'; import Link from 'next/link';
import { GridRow, GridColumn } from 'components/layout/Grid'; import { GridRow, GridColumn } from 'components/layout/Grid';
import BrowsersTable from 'components/metrics/BrowsersTable'; import BrowsersTable from 'components/metrics/BrowsersTable';
@ -14,8 +13,8 @@ import ScreenTable from 'components/metrics/ScreenTable';
import EventsTable from 'components/metrics/EventsTable'; import EventsTable from 'components/metrics/EventsTable';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { labels } from 'components/messages';
import styles from './WebsiteMenuView.module.css'; import styles from './WebsiteMenuView.module.css';
import useMessages from 'hooks/useMessages';
const views = { const views = {
url: PagesTable, url: PagesTable,
@ -31,7 +30,7 @@ const views = {
}; };
export default function WebsiteMenuView({ websiteId, websiteDomain }) { export default function WebsiteMenuView({ websiteId, websiteDomain }) {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
const { const {
resolveUrl, resolveUrl,
query: { view }, query: { view },

View File

@ -1,7 +1,7 @@
SET allow_experimental_object_type = 1; SET allow_experimental_object_type = 1;
-- Create Event -- Create Event
CREATE TABLE event CREATE TABLE umami.event
( (
website_id UUID, website_id UUID,
session_id UUID, session_id UUID,
@ -34,7 +34,7 @@ CREATE TABLE event
ORDER BY (website_id, session_id, created_at) ORDER BY (website_id, session_id, created_at)
SETTINGS index_granularity = 8192; SETTINGS index_granularity = 8192;
CREATE TABLE event_queue ( CREATE TABLE umami.event_queue (
website_id UUID, website_id UUID,
session_id UUID, session_id UUID,
event_id UUID, event_id UUID,
@ -70,7 +70,7 @@ SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input bro
kafka_max_block_size = 1048576, kafka_max_block_size = 1048576,
kafka_skip_broken_messages = 1; kafka_skip_broken_messages = 1;
CREATE MATERIALIZED VIEW event_queue_mv TO event AS CREATE MATERIALIZED VIEW umami.event_queue_mv TO umami.event AS
SELECT website_id, SELECT website_id,
session_id, session_id,
event_id, event_id,
@ -94,9 +94,9 @@ SELECT website_id,
event_type, event_type,
event_name, event_name,
created_at created_at
FROM event_queue; FROM umami.event_queue;
CREATE TABLE event_data CREATE TABLE umami.event_data
( (
website_id UUID, website_id UUID,
session_id UUID, session_id UUID,
@ -115,7 +115,6 @@ CREATE TABLE event_data
ORDER BY (website_id, event_id, event_key, created_at) ORDER BY (website_id, event_id, event_key, created_at)
SETTINGS index_granularity = 8192; SETTINGS index_granularity = 8192;
CREATE TABLE event_data_queue (
website_id UUID, website_id UUID,
session_id UUID, session_id UUID,
event_id UUID, event_id UUID,
@ -137,7 +136,7 @@ SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input bro
kafka_max_block_size = 1048576, kafka_max_block_size = 1048576,
kafka_skip_broken_messages = 1; kafka_skip_broken_messages = 1;
CREATE MATERIALIZED VIEW event_data_queue_mv TO event_data AS CREATE MATERIALIZED VIEW umami.event_data_queue_mv TO umami.event_data AS
SELECT website_id, SELECT website_id,
session_id, session_id,
event_id, event_id,
@ -150,4 +149,4 @@ SELECT website_id,
event_date_value, event_date_value,
event_data_type, event_data_type,
created_at created_at
FROM event_data_queue; FROM umami.event_data_queue;

View File

@ -78,6 +78,31 @@ CREATE TABLE `website_event` (
PRIMARY KEY (`event_id`) PRIMARY KEY (`event_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `event_data` (
`event_id` VARCHAR(36) NOT NULL,
`website_event_id` VARCHAR(36) NOT NULL,
`website_id` VARCHAR(36) NOT NULL,
`session_id` VARCHAR(36) NOT NULL,
`url_path` VARCHAR(500) NOT NULL,
`event_name` VARCHAR(500) NOT NULL,
`event_key` VARCHAR(500) NOT NULL,
`event_string_value` VARCHAR(500) NOT NULL,
`event_numeric_value` DECIMAL(19, 4) NOT NULL,
`event_date_value` TIMESTAMP(0) NULL,
`event_data_type` INTEGER UNSIGNED NOT NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
INDEX `event_data_created_at_idx`(`created_at`),
INDEX `event_data_session_id_idx`(`session_id`),
INDEX `event_data_website_id_idx`(`website_id`),
INDEX `event_data_website_event_id_idx`(`website_event_id`),
INDEX `event_data_website_id_website_event_id_created_at_idx`(`website_id`, `website_event_id`, `created_at`),
INDEX `event_data_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`),
INDEX `event_data_website_id_session_id_website_event_id_created_at_idx`(`website_id`, `session_id`, `website_event_id`, `created_at`),
PRIMARY KEY (`event_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `team` ( CREATE TABLE `team` (
`team_id` VARCHAR(36) NOT NULL, `team_id` VARCHAR(36) NOT NULL,

View File

@ -17,8 +17,8 @@ model User {
updatedAt DateTime? @map("updated_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0) deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
website Website[]
teamUser TeamUser[] teamUser TeamUser[]
Website Website[]
@@map("user") @@map("user")
} }
@ -38,6 +38,9 @@ model Session {
city String? @db.VarChar(50) city String? @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
websiteEvent WebsiteEvent[]
eventData EventData[]
@@index([createdAt]) @@index([createdAt])
@@index([websiteId]) @@index([websiteId])
@@map("session") @@map("session")
@ -56,6 +59,7 @@ model Website {
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
teamWebsite TeamWebsite[] teamWebsite TeamWebsite[]
eventData EventData[]
@@index([userId]) @@index([userId])
@@index([createdAt]) @@index([createdAt])
@ -77,6 +81,9 @@ model WebsiteEvent {
eventType Int @default(1) @map("event_type") @db.UnsignedInt eventType Int @default(1) @map("event_type") @db.UnsignedInt
eventName String? @map("event_name") @db.VarChar(50) eventName String? @map("event_name") @db.VarChar(50)
eventData EventData[]
session Session @relation(fields: [sessionId], references: [id])
@@index([createdAt]) @@index([createdAt])
@@index([sessionId]) @@index([sessionId])
@@index([websiteId]) @@index([websiteId])
@ -85,6 +92,34 @@ model WebsiteEvent {
@@map("website_event") @@map("website_event")
} }
model EventData {
id String @id() @map("event_id") @db.VarChar(36)
websiteEventId String @map("website_event_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
sessionId String @map("session_id") @db.VarChar(36)
urlPath String @map("url_path") @db.VarChar(500)
eventName String @map("event_name") @db.VarChar(500)
eventKey String @map("event_key") @db.VarChar(500)
eventStringValue String @map("event_string_value") @db.VarChar(500)
eventNumericValue Decimal @map("event_numeric_value") @db.Decimal(19,4)
eventDateValue DateTime? @map("event_date_value") @db.Timestamp(0)
eventDataType Int @map("event_data_type") @db.UnsignedInt
createdAt DateTime? @default(now()) @map("created_at")@db.Timestamp(0)
website Website @relation(fields: [websiteId], references: [id])
websiteEvent WebsiteEvent @relation(fields: [websiteEventId], references: [id])
session Session @relation(fields: [sessionId], references: [id])
@@index([createdAt])
@@index([sessionId])
@@index([websiteId])
@@index([websiteEventId])
@@index([websiteId, websiteEventId, createdAt])
@@index([websiteId, sessionId, createdAt])
@@index([websiteId, sessionId, websiteEventId, createdAt])
@@map("event_data")
}
model Team { model Team {
id String @id() @unique() @map("team_id") @db.VarChar(36) id String @id() @unique() @map("team_id") @db.VarChar(36)
name String @db.VarChar(50) name String @db.VarChar(50)

View File

@ -0,0 +1,38 @@
-- CreateTable
CREATE TABLE "event_data" (
"event_id" UUID NOT NULL,
"website_event_id" UUID NOT NULL,
"website_id" UUID NOT NULL,
"session_id" UUID NOT NULL,
"url_path" VARCHAR(500) NOT NULL,
"event_name" VARCHAR(500) NOT NULL,
"event_key" VARCHAR(500) NOT NULL,
"event_string_value" VARCHAR(500) NOT NULL,
"event_numeric_value" DECIMAL(19,4) NOT NULL,
"event_date_value" TIMESTAMPTZ(6),
"event_data_type" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "event_data_pkey" PRIMARY KEY ("event_id")
);
-- CreateIndex
CREATE INDEX "event_data_created_at_idx" ON "event_data"("created_at");
-- CreateIndex
CREATE INDEX "event_data_session_id_idx" ON "event_data"("session_id");
-- CreateIndex
CREATE INDEX "event_data_website_id_idx" ON "event_data"("website_id");
-- CreateIndex
CREATE INDEX "event_data_website_event_id_idx" ON "event_data"("website_event_id");
-- CreateIndex
CREATE INDEX "event_data_website_id_website_event_id_created_at_idx" ON "event_data"("website_id", "website_event_id", "created_at");
-- CreateIndex
CREATE INDEX "event_data_website_id_session_id_created_at_idx" ON "event_data"("website_id", "session_id", "created_at");
-- CreateIndex
CREATE INDEX "event_data_website_id_session_id_website_event_id_created_a_idx" ON "event_data"("website_id", "session_id", "website_event_id", "created_at");

14
hooks/useMessages.js Normal file
View File

@ -0,0 +1,14 @@
import { useIntl, FormattedMessage } from 'react-intl';
import { messages, labels } from 'components/messages';
export default function useMessages() {
const { formatMessage } = useIntl();
function getMessage(id) {
const message = Object.values(messages).find(value => value.id === id);
return message ? formatMessage(message) : id;
}
return { formatMessage, FormattedMessage, messages, labels, getMessage };
}

View File

@ -1,25 +1,23 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
export default function useSticky({ scrollElement = document, defaultSticky = false }) { export default function useSticky({ enabled = true, threshold = 1 }) {
const [isSticky, setIsSticky] = useState(defaultSticky); const [isSticky, setIsSticky] = useState(false);
const ref = useRef(null); const ref = useRef(null);
const initialTop = useRef(null);
useEffect(() => { useEffect(() => {
const handleScroll = () => { let observer;
setIsSticky((scrollElement?.scrollTop ?? window.scrollY) > initialTop.current); const handler = ([entry]) => setIsSticky(entry.intersectionRatio < threshold);
};
if (initialTop.current === null) { if (enabled && ref.current) {
initialTop.current = ref?.current?.offsetTop; observer = new IntersectionObserver(handler, { threshold: [threshold] });
observer.observe(ref.current);
} }
scrollElement.addEventListener('scroll', handleScroll, true);
return () => { return () => {
scrollElement.removeEventListener('scroll', handleScroll, true); if (observer) {
observer.disconnect();
}
}; };
}, [ref, setIsSticky, scrollElement]); }, [ref, enabled, threshold]);
return { ref, isSticky }; return { ref, isSticky };
} }

View File

@ -94,7 +94,7 @@
"node-fetch": "^3.2.8", "node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-basics": "^0.73.0", "react-basics": "^0.74.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-intl": "^5.24.7", "react-intl": "^5.24.7",

View File

@ -1,10 +1,9 @@
import { Row, Column, Flexbox } from 'react-basics'; import { Row, Column, Flexbox } from 'react-basics';
import { useIntl } from 'react-intl';
import AppLayout from 'components/layout/AppLayout'; import AppLayout from 'components/layout/AppLayout';
import { labels } from 'components/messages'; import useMessages from 'hooks/useMessages';
export default function Custom404() { export default function Custom404() {
const { formatMessage } = useIntl(); const { formatMessage, labels } = useMessages();
return ( return (
<AppLayout> <AppLayout>

View File

@ -29,7 +29,7 @@ export default function App({ Component, pageProps }) {
const Wrapper = ({ children }) => <span className={locale}>{children}</span>; const Wrapper = ({ children }) => <span className={locale}>{children}</span>;
if (!config || config.uiDisabled) { if (config?.uiDisabled) {
return null; return null;
} }

View File

@ -115,11 +115,11 @@ async function clickhouseQuery(data: {
subdivision1: subdivision1 ? subdivision1 : null, subdivision1: subdivision1 ? subdivision1 : null,
subdivision2: subdivision2 ? subdivision2 : null, subdivision2: subdivision2 ? subdivision2 : null,
city: city ? city : null, city: city ? city : null,
urlPath: urlPath?.substring(0, URL_LENGTH), url_path: urlPath?.substring(0, URL_LENGTH),
urlQuery: urlQuery?.substring(0, URL_LENGTH), url_query: urlQuery?.substring(0, URL_LENGTH),
referrerPath: referrerPath?.substring(0, URL_LENGTH), referrer_path: referrerPath?.substring(0, URL_LENGTH),
referrerQuery: referrerQuery?.substring(0, URL_LENGTH), referrer_query: referrerQuery?.substring(0, URL_LENGTH),
referrerDomain: referrerDomain?.substring(0, URL_LENGTH), referrer_domain: referrerDomain?.substring(0, URL_LENGTH),
page_title: pageTitle, page_title: pageTitle,
event_type: EVENT_TYPE.pageView, event_type: EVENT_TYPE.pageView,
created_at: getDateFormat(new Date()), created_at: getDateFormat(new Date()),

View File

@ -1,8 +1,3 @@
html {
overflow-x: hidden;
margin-right: calc(-1 * (100vw - 100%));
}
html, html,
body { body {
font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans,
@ -64,7 +59,7 @@ svg {
#__next { #__next {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
flex: 1;
} }

View File

@ -7136,10 +7136,10 @@ rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-basics@^0.73.0: react-basics@^0.74.0:
version "0.73.0" version "0.74.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.73.0.tgz#9555563f3407ac417dc833dfca47588123d55535" resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.74.0.tgz#153433bc485d6b71d8edf377d1a83f1d55133e24"
integrity sha512-eEK8yWWrXO7JATBlPKBfFQlD1hNZoNeEtlYNx+QjOCLKu1qjClutP5nXWHmX4gHE97XFwUKzbTU35NkNEy5C0w== integrity sha512-Z9XwgEOSRvcPqFqFZL6HR59t/XrqhIB8uoYwbmon3IFX2W0kOPqkX1Box0c+2BibJoHp4N4mbfuZWK2kSEnq9g==
dependencies: dependencies:
classnames "^2.3.1" classnames "^2.3.1"
date-fns "^2.29.3" date-fns "^2.29.3"