Merge branch 'dev' into feat/um-197-hook-up-teams

pull/1825/head
Brian Cao 2023-03-08 22:49:29 -08:00
commit f18ee58ac8
41 changed files with 1195 additions and 875 deletions

View File

@ -12,15 +12,15 @@ export default function FilterLink({ id, value, label, externalUrl }) {
return (
<div className={styles.row}>
<Link href={resolveUrl({ [id]: value })} replace>
<a
className={classNames(styles.label, {
[styles.inactive]: active && !selected,
[styles.active]: active && selected,
})}
>
{safeDecodeURI(label || value)}
</a>
<Link
href={resolveUrl({ [id]: value })}
className={classNames(styles.label, {
[styles.inactive]: active && !selected,
[styles.active]: active && selected,
})}
replace
>
{safeDecodeURI(label || value)}
</Link>
{externalUrl && (
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">

View File

@ -16,10 +16,8 @@ export default function MobileMenu({ items = [], onClose }) {
</div>
<div className={styles.items}>
{items.map(({ label, value }) => (
<Link key={value} href={value}>
<a className={styles.item} onClick={onClose}>
{label}
</a>
<Link key={value} href={value} className={styles.item} onClick={onClose}>
{label}
</Link>
))}
</div>

View File

@ -12,17 +12,18 @@ export default function StickyHeader({
}) {
const { ref: scrollRef, isSticky } = useSticky({ scrollElement });
const { ref: measureRef, dimensions } = useMeasure();
const active = enabled && isSticky;
return (
<div
ref={measureRef}
data-sticky={enabled && isSticky}
style={enabled && isSticky ? { height: dimensions.height } : null}
data-sticky={active}
style={active ? { height: dimensions.height } : null}
>
<div
ref={scrollRef}
className={classNames(className, { [stickyClassName]: enabled && isSticky })}
style={enabled && isSticky ? { ...stickyStyle, width: dimensions.width } : null}
className={classNames(className, { [stickyClassName]: active })}
style={active ? { ...stickyStyle, width: dimensions.width } : null}
>
{children}
</div>

View File

@ -1,41 +0,0 @@
import { useState, useRef, useEffect } from 'react';
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return !(
rect.bottom < 0 ||
rect.right < 0 ||
rect.left > window.innerWidth ||
rect.top > window.innerHeight
);
}
export default function CheckVisible({ className, children }) {
const [visible, setVisible] = useState(false);
const ref = useRef();
useEffect(() => {
const checkPosition = () => {
if (ref.current) {
const state = isInViewport(ref.current);
if (state !== visible) {
setVisible(state);
}
}
};
checkPosition();
window.addEventListener('scroll', checkPosition);
return () => {
window.removeEventListener('scroll', checkPosition);
};
}, [visible]);
return (
<div ref={ref} className={className} data-visible={visible}>
{typeof children === 'function' ? children(visible) : children}
</div>
);
}

View File

@ -18,7 +18,7 @@ function DateFilter({ websiteId, value, className }) {
const [showPicker, setShowPicker] = useState(false);
async function handleDateChange(value) {
if (value === 'all') {
if (value === 'all' && websiteId) {
const data = await get(`/websites/${websiteId}`);
if (data) {

View File

@ -7,15 +7,13 @@ export default function LogoutButton({ tooltipPosition = 'top' }) {
const { formatMessage } = useIntl();
return (
<Link href="/logout">
<a>
<Tooltip label={formatMessage(labels.logout)} position={tooltipPosition}>
<Button variant="quiet">
<Icon>
<Icons.Logout />
</Icon>
</Button>
</Tooltip>
</a>
<Tooltip label={formatMessage(labels.logout)} position={tooltipPosition}>
<Button variant="quiet">
<Icon>
<Icons.Logout />
</Icon>
</Button>
</Tooltip>
</Link>
);
}

View File

@ -1,6 +1,6 @@
import { useIntl } from 'react-intl';
import { LoadingButton, Icon, Tooltip } from 'react-basics';
import { setDateRange } from 'store/websites';
import { setWebsiteDateRange } from 'store/websites';
import useDateRange from 'hooks/useDateRange';
import Icons from 'components/icons';
import { labels } from 'components/messages';
@ -12,9 +12,9 @@ function RefreshButton({ websiteId, isLoading }) {
function handleClick() {
if (!isLoading && dateRange) {
if (/^\d+/.test(dateRange.value)) {
setDateRange(websiteId, dateRange.value);
setWebsiteDateRange(websiteId, dateRange.value);
} else {
setDateRange(websiteId, dateRange);
setWebsiteDateRange(websiteId, dateRange);
}
}
}

View File

@ -18,7 +18,12 @@ export default function SettingsButton() {
</Icon>
</Button>
</Tooltip>
<Popup className={styles.popup} position="bottom" alignment="end">
<Popup
className={styles.popup}
position="bottom"
alignment="end"
onClick={e => e.stopPropagation()}
>
<Form>
<FormRow label={formatMessage(labels.timezone)}>
<TimezoneSetting />

View File

@ -1,80 +0,0 @@
import useConfig from 'hooks/useConfig';
import useUser from 'hooks/useUser';
import { AUTH_TOKEN } from 'lib/constants';
import { removeItem } from 'next-basics';
import { useRouter } from 'next/router';
import { useRef, useState } from 'react';
import { Button, Icon, Item, Menu, Popup, Text } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import useDocumentClick from 'hooks/useDocumentClick';
import Profile from 'assets/profile.svg';
import styles from './UserButton.module.css';
export default function UserButton() {
const [show, setShow] = useState(false);
const ref = useRef();
const { user } = useUser();
const router = useRouter();
const { adminDisabled } = useConfig();
const menuOptions = [
{
label: (
<FormattedMessage
id="label.logged-in-as"
defaultMessage="Logged in as {username}"
values={{ username: <b>{user.username}</b> }}
/>
),
value: 'username',
className: styles.username,
},
{
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
value: 'profile',
hidden: adminDisabled,
divider: true,
},
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
];
function handleClick() {
setShow(state => !state);
}
function handleSelect(value) {
if (value === 'logout') {
removeItem(AUTH_TOKEN);
router.push('/login');
} else if (value === 'profile') {
router.push('/profile');
}
}
useDocumentClick(e => {
if (!ref.current?.contains(e.target)) {
setShow(false);
}
});
return (
<div className={styles.button} ref={ref}>
<Button variant="light" onClick={handleClick}>
<Icon className={styles.icon} size="large">
<Profile />
</Icon>
</Button>
{show && (
<Popup className={styles.menu} position="bottom" gap={5}>
<Menu items={menuOptions} onSelect={handleSelect}>
{({ label, value }) => (
<Item key={value}>
<Text>{label}</Text>
</Item>
)}
</Menu>
</Popup>
)}
</div>
);
}

View File

@ -1,25 +0,0 @@
.button {
position: relative;
}
.username {
border-bottom: 1px solid var(--base500);
}
.username:hover {
background: var(--base50);
}
.icon svg {
height: 16px;
width: 16px;
}
.menu {
left: -50%;
background: var(--base50);
border: 1px solid var(--base500);
border-radius: 4px;
overflow: hidden;
z-index: 100;
}

View File

@ -8,7 +8,7 @@ export default function Footer() {
return (
<footer className={styles.footer}>
<Row>
<Column defaultSize={11} xs={12} sm={12}>
<Column defaultSize={12} lg={11} xl={11}>
<div>
<FormattedMessage
{...labels.poweredBy}
@ -22,7 +22,7 @@ export default function Footer() {
/>
</div>
</Column>
<Column className={styles.version} defaultSize={1} xs={12} sm={12}>
<Column className={styles.version} defaultSize={12} lg={1} xl={1}>
<a href={REPO_URL}>{`v${CURRENT_VERSION}`}</a>
</Column>
</Row>

View File

@ -1,6 +1,7 @@
.footer {
font-size: var(--font-size-sm);
text-align: center;
line-height: 30px;
margin: 60px 0;
}
@ -11,10 +12,5 @@
.version {
text-align: right;
padding-right: 10px;
}
@media only screen and (max-width: 768px) {
.footer .version {
text-align: center;
}
white-space: nowrap;
}

View File

@ -11,13 +11,11 @@ export default function Header() {
<header className={styles.header}>
<Row>
<Column>
<Link href="https://umami.is" target="_blank">
<a className={styles.title}>
<Icon size="lg">
<Icons.Logo />
</Icon>
<Text>umami</Text>
</a>
<Link href="https://umami.is" target="_blank" className={styles.title}>
<Icon size="lg">
<Icons.Logo />
</Icon>
<Text>umami</Text>
</Link>
</Column>
<Column className={styles.buttons}>

View File

@ -1,38 +0,0 @@
import { useRouter } from 'next/router';
import classNames from 'classnames';
import NavMenu from 'components/common/NavMenu';
import styles from './MenuLayout.module.css';
export default function MenuLayout({
menu,
selectedOption,
className,
menuClassName,
contentClassName,
children,
replace = false,
}) {
const router = useRouter();
function handleSelect(url) {
if (replace) {
router.replace(url, undefined, { shallow: true });
} else {
router.push(url, undefined, { shallow: true });
}
}
return (
<div className={classNames(styles.container, className, 'row')}>
<NavMenu
options={menu}
selectedOption={selectedOption}
className={classNames(styles.menu, menuClassName, 'col-12 col-lg-2')}
onSelect={handleSelect}
/>
<div className={classNames(styles.content, contentClassName, 'col-12 col-lg-10')}>
{children}
</div>
</div>
);
}

View File

@ -1,38 +0,0 @@
.container {
display: flex;
flex: 1;
height: 100%;
}
.container .menu {
padding: 30px 0;
border: 0;
}
.container .content {
flex: 1;
position: relative;
border-left: 1px solid var(--base300);
padding-left: 30px;
margin-left: 30px;
}
@media only screen and (max-width: 992px) {
.container {
flex-direction: column;
height: auto;
}
.container .menu {
display: flex;
justify-content: space-around;
align-items: flex-start;
}
.container .content {
border-top: 1px solid var(--base300);
border-left: 0;
padding-left: 0;
margin-left: 0;
}
}

View File

@ -99,6 +99,12 @@ export const labels = defineMessages({
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' },
logs: { id: 'label.activity-log', defaultMessage: 'Activity log' },
dismiss: { id: 'label.dismiss', defaultMessage: 'Dismiss' },
poweredBy: { id: 'label.powered-by', defaultMessage: 'Powered by {name}' },
pageViews: { id: 'label.page-views', defaultMessage: 'Page views' },
uniqueVisitors: { id: 'label.unique-visitors', defaultMessage: 'Unique visitors' },
bounceRate: { id: 'label.bounce-rate', defaultMessage: 'Bounce rate' },
averageVisitTime: { id: 'label.average-visit-time', defaultMessage: 'Average visit time' },
});
export const messages = defineMessages({

View File

@ -1,16 +1,17 @@
import { useState } from 'react';
import { Loading } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useIntl } from 'react-intl';
import ErrorMessage from 'components/common/ErrorMessage';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard';
import { labels } from 'components/messages';
import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId, className }) {
export default function MetricsBar({ websiteId }) {
const { formatMessage } = useIntl();
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
@ -52,25 +53,25 @@ export default function MetricsBar({ websiteId, className }) {
};
return (
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
<div className={styles.bar} onClick={handleSetFormat}>
{isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
{data && !error && isFetched && (
<>
<MetricCard
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
label={formatMessage(labels.views)}
value={pageviews.value}
change={pageviews.change}
format={formatFunc}
/>
<MetricCard
label={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
label={formatMessage(labels.visitors)}
value={uniques.value}
change={uniques.change}
format={formatFunc}
/>
<MetricCard
label={<FormattedMessage id="metrics.bounce-rate" defaultMessage="Bounce rate" />}
label={formatMessage(labels.bounceRate)}
value={uniques.value ? (num / uniques.value) * 100 : 0}
change={
uniques.value && uniques.change
@ -82,12 +83,7 @@ export default function MetricsBar({ websiteId, className }) {
reverseColors
/>
<MetricCard
label={
<FormattedMessage
id="metrics.average-visit-time"
defaultMessage="Average visit time"
/>
}
label={formatMessage(labels.averageVisitTime)}
value={
totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value)

View File

@ -80,14 +80,12 @@ export default function MetricsTable({
<div className={styles.footer}>
{data && !error && limit && (
<Link href={router.pathname} as={resolveUrl({ view: type })}>
<a>
<Button variant="quiet">
<Text>{formatMessage(messages.more)}</Text>
<Icon size="sm">
<Icons.ArrowRight />
</Icon>
</Button>
</a>
<Button variant="quiet">
<Text>{formatMessage(messages.more)}</Text>
<Icon size="sm">
<Icons.ArrowRight />
</Icon>
</Button>
</Link>
)}
</div>

View File

@ -1,9 +1,11 @@
import { useVisible } from 'react-basics';
import { useIntl } from 'react-intl';
import { colord } from 'colord';
import CheckVisible from 'components/helpers/CheckVisible';
import BarChart from './BarChart';
import useTheme from 'hooks/useTheme';
import { THEME_COLORS, DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { labels } from 'components/messages';
import { useMemo } from 'react';
export default function PageviewsChart({
websiteId,
@ -15,19 +17,23 @@ export default function PageviewsChart({
animationDuration = DEFAULT_ANIMATION_DURATION,
...props
}) {
const intl = useIntl();
const { formatMessage } = useIntl();
const [theme] = useTheme();
const primaryColor = colord(THEME_COLORS[theme].primary);
const colors = {
views: {
background: primaryColor.alpha(0.4).toRgbString(),
border: primaryColor.alpha(0.5).toRgbString(),
},
visitors: {
background: primaryColor.alpha(0.6).toRgbString(),
border: primaryColor.alpha(0.7).toRgbString(),
},
};
const { ref, visible } = useVisible();
const colors = useMemo(() => {
const primaryColor = colord(THEME_COLORS[theme].primary);
return {
views: {
background: primaryColor.alpha(0.4).toRgbString(),
border: primaryColor.alpha(0.5).toRgbString(),
},
visitors: {
background: primaryColor.alpha(0.6).toRgbString(),
border: primaryColor.alpha(0.7).toRgbString(),
},
};
}, [theme]);
const handleUpdate = chart => {
const {
@ -35,15 +41,9 @@ export default function PageviewsChart({
} = chart;
datasets[0].data = data.sessions;
datasets[0].label = intl.formatMessage({
id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors',
});
datasets[0].label = formatMessage(labels.uniqueVisitors);
datasets[1].data = data.pageviews;
datasets[1].label = intl.formatMessage({
id: 'metrics.page-views',
defaultMessage: 'Page views',
});
datasets[1].label = formatMessage(labels.pageViews);
};
if (!data) {
@ -51,43 +51,35 @@ export default function PageviewsChart({
}
return (
<CheckVisible>
{visible => (
<BarChart
{...props}
className={className}
chartId={websiteId}
datasets={[
{
label: intl.formatMessage({
id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors',
}),
data: data.sessions,
lineTension: 0,
backgroundColor: colors.visitors.background,
borderColor: colors.visitors.border,
borderWidth: 1,
},
{
label: intl.formatMessage({
id: 'metrics.page-views',
defaultMessage: 'Page views',
}),
data: data.pageviews,
lineTension: 0,
backgroundColor: colors.views.background,
borderColor: colors.views.border,
borderWidth: 1,
},
]}
unit={unit}
records={records}
animationDuration={visible ? animationDuration : 0}
onUpdate={handleUpdate}
loading={loading}
/>
)}
</CheckVisible>
<div ref={ref}>
<BarChart
{...props}
className={className}
chartId={websiteId}
datasets={[
{
label: formatMessage(labels.uniqueVisitors),
data: data.sessions,
lineTension: 0,
backgroundColor: colors.visitors.background,
borderColor: colors.visitors.border,
borderWidth: 1,
},
{
label: formatMessage(labels.pageViews),
data: data.pageviews,
lineTension: 0,
backgroundColor: colors.views.background,
borderColor: colors.views.border,
borderWidth: 1,
},
]}
unit={unit}
records={records}
animationDuration={visible ? animationDuration : 0}
onUpdate={handleUpdate}
loading={loading}
/>
</div>
);
}

View File

@ -6,7 +6,7 @@ import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/input/DateFilter';
import StickyHeader from 'components/helpers/StickyHeader';
import StickyHeader from 'components/common/StickyHeader';
import ErrorMessage from 'components/common/ErrorMessage';
import FilterTags from 'components/metrics/FilterTags';
import RefreshButton from 'components/input/RefreshButton';
@ -71,14 +71,12 @@ export default function WebsiteChart({
<WebsiteHeader websiteId={websiteId} title={title} domain={domain}>
{showDetailsButton && (
<Link href={`/websites/${websiteId}`}>
<a>
<Button>
<Text>{formatMessage(labels.viewDetails)}</Text>
<Icon>
<Icons.ArrowRight />
</Icon>
</Button>
</a>
<Button>
<Text>{formatMessage(labels.viewDetails)}</Text>
<Icon>
<Icons.ArrowRight />
</Icon>
</Button>
</Link>
)}
</WebsiteHeader>

View File

@ -59,26 +59,25 @@ export default function TestConsole() {
<Column xs="4">
<div className={styles.header}>Page links</div>
<div>
<Link href={`/console/${websiteId}?page=1`}>
<a>page one</a>
</Link>
<Link href={`/console/${websiteId}?page=1`}>page one</Link>
</div>
<div>
<Link href={`/console/${websiteId}?page=2`}>
<a>page two</a>
</Link>
<Link href={`/console/${websiteId}?page=2`}>page two</Link>
</div>
<div>
<Link href={`https://www.google.com`}>
<a className="umami--click--external-link-direct">external link (direct)</a>
</Link>
<a href="https://www.google.com" className="umami--click--external-link-direct">
external link (direct)
</a>
</div>
<div>
<Link href={`https://www.google.com`}>
<a className="umami--click--external-link-tab" target="_blank">
external link (tab)
</a>
</Link>
<a
href="https://www.google.com"
className="umami--click--external-link-tab"
target="_blank"
rel="noreferrer"
>
external link (tab)
</a>
</div>
</Column>
<Column xs="4">

View File

@ -6,7 +6,7 @@ import firstBy from 'thenby';
import { GridRow, GridColumn } from 'components/layout/Grid';
import Page from 'components/layout/Page';
import RealtimeChart from 'components/metrics/RealtimeChart';
import StickyHeader from 'components/helpers/StickyHeader';
import StickyHeader from 'components/common/StickyHeader';
import PageHeader from 'components/layout/PageHeader';
import WorldMap from 'components/common/WorldMap';
import RealtimeLog from 'components/pages/realtime/RealtimeLog';

View File

@ -14,7 +14,7 @@ export default function DateRangeSetting() {
return (
<Flexbox width={400} gap={10}>
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} />
<DateFilter value={value} startDate={startDate} endDate={endDate} />
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
</Flexbox>
);

View File

@ -53,14 +53,12 @@ export default function TeamsTable({ data = [], onDelete }) {
action: (
<Flexbox flex={1} gap={10} justifyContent="end">
<Link href={`/settings/teams/${id}`}>
<a>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</a>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
{showDelete && (
<ModalTrigger>

View File

@ -59,15 +59,13 @@ export default function WebsiteSettings({ websiteId }) {
</Breadcrumbs>
}
>
<Link href={`/websites/${websiteId}`}>
<a target="_blank">
<Button variant="primary">
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</a>
<Link href={`/analytics/websites/${websiteId}`} target="_blank">
<Button variant="primary">
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
</PageHeader>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>

View File

@ -42,24 +42,20 @@ export default function WebsitesTable({ data = [] }) {
row.action = (
<Flexbox flex={1} justifyContent="end" gap={10}>
<Link href={`/settings/websites/${id}`}>
<a>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</a>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
<Link href={`/websites/${id}`}>
<a>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</a>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
</Flexbox>
);

View File

@ -86,22 +86,20 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) {
<GridRow>
<GridColumn xs={12} sm={12} md={12} defaultSize={3} className={styles.menu}>
<Link href={resolveUrl({ view: undefined })}>
<a>
<Flexbox justifyContent="center">
<Button variant="quiet">
<Icon rotate={180}>
<Icons.ArrowRight />
</Icon>
<Text>{formatMessage(labels.back)}</Text>
</Button>
</Flexbox>
</a>
<Flexbox justifyContent="center">
<Button variant="quiet">
<Icon rotate={180}>
<Icons.ArrowRight />
</Icon>
<Text>{formatMessage(labels.back)}</Text>
</Button>
</Flexbox>
</Link>
<Menu items={items} selectedKey={view}>
{({ key, label }) => (
<Item key={key} className={styles.item}>
<Link href={resolveUrl({ view: key })} shallow={true}>
<a>{label}</a>
{label}
</Link>
</Item>
)}

View File

@ -1,42 +1,26 @@
import { useCallback, useMemo } from 'react';
import { parseISO } from 'date-fns';
import { getDateRange } from 'lib/date';
import { getItem, setItem } from 'next-basics';
import { parseDateRange } from 'lib/date';
import { setItem } from 'next-basics';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
import useForceUpdate from './useForceUpdate';
import useLocale from './useLocale';
import useStore, { setDateRange } from 'store/websites';
import websiteStore, { setWebsiteDateRange } from 'store/websites';
import appStore, { setDateRange } from 'store/app';
export default function useDateRange(websiteId) {
const { locale } = useLocale();
const forceUpdate = useForceUpdate();
const selector = useCallback(state => state?.[websiteId]?.dateRange, [websiteId]);
const websiteDateRange = useStore(selector);
const defaultDateRange = useMemo(() => getDateRange(DEFAULT_DATE_RANGE, locale), [locale]);
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
const defaultConfig = DEFAULT_DATE_RANGE;
const globalConfig = appStore(state => state.dateRange);
const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
const globalDefault = getItem(DATE_RANGE_CONFIG);
let globalDateRange;
if (globalDefault) {
if (typeof globalDefault === 'string') {
globalDateRange = getDateRange(globalDefault, locale);
} else if (typeof globalDefault === 'object') {
globalDateRange = {
...globalDefault,
startDate: parseISO(globalDefault.startDate),
endDate: parseISO(globalDefault.endDate),
};
}
}
function saveDateRange(dateRange) {
function saveDateRange(value) {
if (websiteId) {
setDateRange(websiteId, dateRange);
setWebsiteDateRange(websiteId, value);
} else {
setItem(DATE_RANGE_CONFIG, dateRange);
forceUpdate();
setItem(DATE_RANGE_CONFIG, value);
setDateRange(value);
}
}
return [websiteDateRange || globalDateRange || defaultDateRange, saveDateRange];
return [dateRange, saveDateRange];
}

View File

@ -26,6 +26,7 @@ import {
differenceInCalendarMonths,
differenceInCalendarYears,
format,
parseISO,
} from 'date-fns';
import { getDateLocale } from 'lib/lang';
@ -37,7 +38,16 @@ export function getLocalTime(t) {
return addMinutes(new Date(t), new Date().getTimezoneOffset());
}
export function getDateRange(value, locale = 'en-US') {
export function parseDateRange(value, locale = 'en-US') {
if (typeof value === 'object') {
const { startDate, endDate } = value;
return {
...value,
startDate: typeof startDate === 'string' ? parseISO(startDate) : startDate,
endDate: typeof endDate === 'string' ? parseISO(endDate) : endDate,
};
}
const now = new Date();
const dateLocale = getDateLocale(locale);

View File

@ -44,7 +44,9 @@ export const useAuth = createMiddleware(async (req, res, next) => {
user = await redis.get(authKey);
}
log({ token, payload, user, shareToken });
if (process.env.NODE_ENV === 'development') {
log({ token, shareToken, payload, user });
}
if (!user?.id && !shareToken) {
log('useAuth: User not authorized');

View File

@ -2,6 +2,8 @@
require('dotenv').config();
const pkg = require('./package.json');
const CLOUD_URL = 'https://cloud.umami.is';
const contentSecurityPolicy = `
default-src 'self';
img-src *;
@ -33,11 +35,29 @@ if (process.env.FORCE_SSL) {
});
}
module.exports = {
const rewrites = [];
if (process.env.COLLECT_API_ENDPOINT) {
rewrites.push({
source: process.env.COLLECT_API_ENDPOINT,
destination: '/api/send',
});
}
const redirects = [];
if (process.env.CLOUD_MODE) {
redirects.push({
source: '/login',
destination: CLOUD_URL,
permanent: false,
});
}
const config = {
env: {
currentVersion: pkg.version,
isProduction: process.env.NODE_ENV === 'production',
uiDisabled: !!process.env.DISABLE_UI,
},
basePath: process.env.BASE_PATH,
output: 'standalone',
@ -66,10 +86,16 @@ module.exports = {
},
async rewrites() {
return [
...rewrites,
{
source: '/telemetry.js',
destination: '/api/scripts/telemetry',
},
];
},
async redirects() {
return [...redirects];
},
};
module.exports = config;

View File

@ -10,7 +10,7 @@
"url": "https://github.com/umami-software/umami.git"
},
"scripts": {
"dev": "next dev",
"dev": "next dev -p 3000",
"build": "npm-run-all build-db check-db build-tracker build-geo build-app",
"start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
@ -59,7 +59,7 @@
],
"dependencies": {
"@fontsource/inter": "4.5.7",
"@prisma/client": "4.9.0",
"@prisma/client": "4.11.0",
"@tanstack/react-query": "^4.16.1",
"@umami/prisma-client": "^0.2.0",
"@umami/redis-client": "^0.2.0",
@ -88,12 +88,12 @@
"kafkajs": "^2.1.0",
"maxmind": "^4.3.6",
"moment-timezone": "^0.5.35",
"next": "^12.3.1",
"next": "^13.2.3",
"next-basics": "^0.27.0",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"react": "^18.2.0",
"react-basics": "^0.68.0",
"react-basics": "^0.70.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-intl": "^5.24.7",
@ -134,7 +134,7 @@
"postcss-preset-env": "7.8.3",
"postcss-rtlcss": "^4.0.1",
"prettier": "^2.6.2",
"prisma": "4.9.0",
"prisma": "4.11.0",
"prompts": "2.4.2",
"rollup": "^2.70.1",
"rollup-plugin-terser": "^7.0.2",

View File

@ -9,7 +9,7 @@ import 'styles/variables.css';
import 'styles/locale.css';
import 'styles/index.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import Script from 'next/script';
const client = new QueryClient({
@ -24,11 +24,11 @@ const client = new QueryClient({
export default function App({ Component, pageProps }) {
const { locale, messages } = useLocale();
const { basePath, pathname } = useRouter();
useConfig();
const config = useConfig();
const Wrapper = ({ children }) => <span className={locale}>{children}</span>;
if (process.env.uiDisabled) {
if (!config || config.uiDisabled) {
return null;
}

View File

@ -6,7 +6,6 @@ export interface ConfigResponse {
trackerScriptName: string;
updatesDisabled: boolean;
telemetryDisabled: boolean;
adminDisabled: boolean;
cloudMode: boolean;
}
@ -17,8 +16,7 @@ export default async (req: NextApiRequest, res: NextApiResponse<ConfigResponse>)
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
updatesDisabled: !!process.env.DISABLE_UPDATES,
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
adminDisabled: !!process.env.DISABLE_ADMIN,
cloudMode: process.env.CLOUD_MODE,
cloudMode: !!process.env.CLOUD_MODE,
});
}

View File

@ -16,7 +16,7 @@ export default function LoginPage({ disabled }) {
export async function getServerSideProps() {
return {
props: {
disabled: !!(process.env.DISABLE_LOGIN || process.env.CLOUD_MODE),
disabled: !!process.env.DISABLE_LOGIN,
},
};
}

View File

@ -1,11 +1,9 @@
export default () => null;
export async function getServerSideProps() {
const destination = process.env.CLOUD_MODE ? 'https://cloud.umami.is' : '/settings/websites';
return {
redirect: {
destination,
destination: '/settings/websites',
permanent: true,
},
};

View File

@ -1,10 +1,18 @@
import create from 'zustand';
import { DEFAULT_LOCALE, DEFAULT_THEME, LOCALE_CONFIG, THEME_CONFIG } from 'lib/constants';
import {
DATE_RANGE_CONFIG,
DEFAULT_DATE_RANGE,
DEFAULT_LOCALE,
DEFAULT_THEME,
LOCALE_CONFIG,
THEME_CONFIG,
} from 'lib/constants';
import { getItem } from 'next-basics';
const initialState = {
locale: getItem(LOCALE_CONFIG) || DEFAULT_LOCALE,
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
dateRange: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE,
shareToken: null,
user: null,
config: null,
@ -32,4 +40,8 @@ export function setConfig(config) {
store.setState({ config });
}
export function setDateRange(dateRange) {
store.setState({ dateRange });
}
export default store;

View File

@ -1,11 +1,15 @@
import create from 'zustand';
import produce from 'immer';
import app from './app';
import { getDateRange } from 'lib/date';
import { parseDateRange } from 'lib/date';
const store = create(() => ({}));
export function setDateRange(websiteId, value) {
export function getWebsiteDateRange(websiteId) {
return store.getState()?.[websiteId];
}
export function setWebsiteDateRange(websiteId, value) {
store.setState(
produce(state => {
if (!state[websiteId]) {
@ -16,7 +20,7 @@ export function setDateRange(websiteId, value) {
if (typeof value === 'string') {
const { locale } = app.getState();
dateRange = getDateRange(value, locale);
dateRange = parseDateRange(value, locale);
}
state[websiteId].dateRange = { ...dateRange, modified: Date.now() };

View File

@ -47,7 +47,7 @@
(dnt && doNotTrack()) ||
(domain && !domains.includes(hostname));
const tracker_delay_duration = 300;
const delayDuration = 300;
const _data = 'data-';
const _false = 'false';
const attr = currentScript.getAttribute.bind(currentScript);
@ -192,7 +192,7 @@
}
if (currentUrl !== currentRef) {
setTimeout(() => trackView(), tracker_delay_duration);
setTimeout(() => trackView(), delayDuration);
}
};

1341
yarn.lock

File diff suppressed because it is too large Load Diff