More refactoring.

This commit is contained in:
Mike Cao 2023-01-30 21:44:07 -08:00
parent 5f15ad0807
commit 02a1438cfe
41 changed files with 196 additions and 721 deletions

View File

@ -1,9 +1,9 @@
import { Icon, Button, PopupTrigger, Popup, Tooltip, Icons, Text } from 'react-basics';
import { Icon, Button, PopupTrigger, Popup, Tooltip, Text } from 'react-basics';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import { languages } from 'lib/lang';
import useLocale from 'hooks/useLocale';
import { Globe } from 'components/icons';
import Icons from 'components/icons';
import { labels } from 'components/messages';
import styles from './LanguageButton.module.css';
@ -21,7 +21,7 @@ export default function LanguageButton({ tooltipPosition = 'top' }) {
<PopupTrigger action="hover">
<Button variant="quiet">
<Icon>
<Globe />
<Icons.Globe />
</Icon>
</Button>
<Tooltip position={tooltipPosition}>{formatMessage(labels.language)}</Tooltip>

View File

@ -2,7 +2,7 @@ import { useTransition, animated } from 'react-spring';
import { Button, Icon, PopupTrigger, Tooltip } from 'react-basics';
import { useIntl } from 'react-intl';
import useTheme from 'hooks/useTheme';
import { Sun, Moon } from 'components/icons';
import Icons from 'components/icons';
import { labels } from 'components/messages';
import styles from './ThemeButton.module.css';
@ -28,11 +28,11 @@ export default function ThemeButton({ tooltipPosition = 'top' }) {
}
return (
<PopupTrigger action="hover" popupProps={{ position: 'top' }}>
<PopupTrigger action="hover">
<Button variant="quiet" className={styles.button} onClick={handleClick}>
{transitions((style, item) => (
<animated.div key={item} style={style}>
<Icon className={styles.icon}>{item === 'light' ? <Sun /> : <Moon />}</Icon>
<Icon className={styles.icon}>{item === 'light' ? <Icons.Sun /> : <Icons.Moon />}</Icon>
</animated.div>
))}
</Button>

View File

@ -13,7 +13,7 @@ import styles from './UserButton.module.css';
export default function UserButton() {
const [show, setShow] = useState(false);
const ref = useRef();
const user = useUser();
const { user } = useUser();
const router = useRouter();
const { adminDisabled } = useConfig();

View File

@ -1,280 +0,0 @@
import { useState } from 'react';
import classNames from 'classnames';
import {
startOfWeek,
startOfMonth,
startOfYear,
endOfMonth,
addDays,
subDays,
addYears,
subYears,
addMonths,
setMonth,
setYear,
isSameDay,
isBefore,
isAfter,
} from 'date-fns';
import { Button, Icon, Icons } from 'react-basics';
import { chunkArray } from 'next-basics';
import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date';
import { getDateLocale } from 'lib/lang';
import styles from './Calendar.module.css';
export default function Calendar({ date, minDate, maxDate, onChange }) {
const { locale } = useLocale();
const [selectMonth, setSelectMonth] = useState(false);
const [selectYear, setSelectYear] = useState(false);
const month = dateFormat(date, 'MMMM', locale);
const year = date.getFullYear();
function toggleMonthSelect() {
setSelectYear(false);
setSelectMonth(state => !state);
}
function toggleYearSelect() {
setSelectMonth(false);
setSelectYear(state => !state);
}
function handleChange(value) {
setSelectMonth(false);
setSelectYear(false);
if (value) {
onChange(value);
}
}
return (
<div className={styles.calendar}>
<div className={styles.header}>
<div>{date.getDate()}</div>
<div
className={classNames(styles.selector, { [styles.open]: selectMonth })}
onClick={toggleMonthSelect}
>
{month}
<Icon className={styles.icon} size="small">
{selectMonth ? <Icons.Close /> : <Icons.ChevronDown />}
</Icon>
</div>
<div
className={classNames(styles.selector, { [styles.open]: selectYear })}
onClick={toggleYearSelect}
>
{year}
<Icon className={styles.icon} size="small">
{selectMonth ? <Icons.Close /> : <Icons.ChevronDown />}
</Icon>
</div>
</div>
<div className={styles.body}>
{!selectMonth && !selectYear && (
<DaySelector
date={date}
minDate={minDate}
maxDate={maxDate}
locale={locale}
onSelect={handleChange}
/>
)}
{selectMonth && (
<MonthSelector
date={date}
minDate={minDate}
maxDate={maxDate}
locale={locale}
onSelect={handleChange}
onClose={toggleMonthSelect}
/>
)}
{selectYear && (
<YearSelector
date={date}
minDate={minDate}
maxDate={maxDate}
onSelect={handleChange}
onClose={toggleYearSelect}
/>
)}
</div>
</div>
);
}
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
const dateLocale = getDateLocale(locale);
const weekStartsOn = dateLocale?.options?.weekStartsOn || 0;
const startWeek = startOfWeek(date, {
locale: dateLocale,
weekStartsOn,
});
const startMonth = startOfMonth(date);
const startDay = subDays(startMonth, startMonth.getDay() - weekStartsOn);
const month = date.getMonth();
const year = date.getFullYear();
const daysOfWeek = [];
for (let i = 0; i < 7; i++) {
daysOfWeek.push(addDays(startWeek, i));
}
const days = [];
for (let i = 0; i < 35; i++) {
days.push(addDays(startDay, i));
}
return (
<table>
<thead>
<tr>
{daysOfWeek.map((day, i) => (
<th key={i} className={locale}>
{dateFormat(day, 'EEE', locale)}
</th>
))}
</tr>
</thead>
<tbody>
{chunkArray(days, 7).map((week, i) => (
<tr key={i}>
{week.map((day, j) => {
const disabled = isBefore(day, minDate) || isAfter(day, maxDate);
return (
<td
key={j}
className={classNames({
[styles.selected]: isSameDay(date, day),
[styles.faded]: day.getMonth() !== month || day.getFullYear() !== year,
[styles.disabled]: disabled,
})}
onClick={!disabled ? () => onSelect(day) : null}
>
{day.getDate()}
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
};
const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => {
const start = startOfYear(date);
const months = [];
for (let i = 0; i < 12; i++) {
months.push(addMonths(start, i));
}
function handleSelect(value) {
onSelect(setMonth(date, value));
}
return (
<table>
<tbody>
{chunkArray(months, 3).map((row, i) => (
<tr key={i}>
{row.map((month, j) => {
const disabled =
isBefore(endOfMonth(month), minDate) || isAfter(startOfMonth(month), maxDate);
return (
<td
key={j}
className={classNames(locale, {
[styles.selected]: month.getMonth() === date.getMonth(),
[styles.disabled]: disabled,
})}
onClick={!disabled ? () => handleSelect(month.getMonth()) : null}
>
{dateFormat(month, 'MMMM', locale)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
};
const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
const [currentDate, setCurrentDate] = useState(date);
const year = date.getFullYear();
const currentYear = currentDate.getFullYear();
const minYear = minDate.getFullYear();
const maxYear = maxDate.getFullYear();
const years = [];
for (let i = 0; i < 15; i++) {
years.push(currentYear - 7 + i);
}
function handleSelect(value) {
onSelect(setYear(date, value));
}
function handlePrevClick() {
setCurrentDate(state => subYears(state, 15));
}
function handleNextClick() {
setCurrentDate(state => addYears(state, 15));
}
return (
<div className={styles.pager}>
<div className={styles.left}>
<Button
size="small"
onClick={handlePrevClick}
disabled={years[0] <= minYear}
variant="light"
>
<Icon>
<Icons.ChevronDown />
</Icon>
</Button>
</div>
<div className={styles.middle}>
<table>
<tbody>
{chunkArray(years, 5).map((row, i) => (
<tr key={i}>
{row.map((n, j) => (
<td
key={j}
className={classNames({
[styles.selected]: n === year,
[styles.disabled]: n < minYear || n > maxYear,
})}
onClick={() => (n < minYear || n > maxYear ? null : handleSelect(n))}
>
{n}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className={styles.right}>
<Button
size="small"
onClick={handleNextClick}
disabled={years[years.length - 1] > maxYear}
variant="light"
>
<Icon>
<Icons.ChevronDown />
</Icon>
</Button>
</div>
</div>
);
};

View File

@ -1,110 +0,0 @@
.calendar {
display: flex;
flex-direction: column;
flex: 1;
min-height: 306px;
}
.calendar table {
width: 100%;
border-spacing: 5px;
}
.calendar td {
color: var(--base800);
cursor: pointer;
text-align: center;
vertical-align: center;
height: 40px;
width: 40px;
border-radius: 5px;
border: 1px solid transparent;
}
.calendar td:hover {
border: 1px solid var(--base300);
background: var(--base75);
}
.calendar td.faded {
color: var(--base500);
}
.calendar td.selected {
font-weight: 600;
border: 1px solid var(--base600);
}
.calendar td.selected:hover {
background: transparent;
}
.calendar td.disabled {
color: var(--base400);
background: var(--base75);
}
.calendar td.disabled:hover {
cursor: default;
background: var(--base75);
border-color: transparent;
}
.calendar td.faded.disabled {
background: var(--base100);
}
.header {
display: flex;
justify-content: space-evenly;
align-items: center;
font-weight: 700;
line-height: 40px;
font-size: var(--font-size-md);
}
.body {
display: flex;
}
.selector {
cursor: pointer;
}
.pager {
display: flex;
flex: 1;
}
.pager button {
align-self: center;
}
.middle {
flex: 1;
}
.left,
.right {
display: flex;
justify-content: center;
align-items: center;
}
.left svg {
transform: rotate(90deg);
}
.right svg {
transform: rotate(-90deg);
}
.icon {
margin-left: 10px;
}
@media only screen and (max-width: 992px) {
.calendar table {
max-width: calc(100vw - 30px);
}
}

View File

@ -1,37 +0,0 @@
import { useState } from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-basics';
import { FormattedMessage } from 'react-intl';
const defaultText = (
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
);
function CopyButton({ element, ...props }) {
const [text, setText] = useState(defaultText);
function handleClick() {
if (element?.current) {
element.current.select();
document.execCommand('copy');
setText(<FormattedMessage id="message.copied" defaultMessage="Copied!" />);
window.getSelection().removeAllRanges();
}
}
return (
<Button {...props} onClick={handleClick}>
{text}
</Button>
);
}
CopyButton.propTypes = {
element: PropTypes.shape({
current: PropTypes.shape({
select: PropTypes.func.isRequired,
}),
}),
};
export default CopyButton;

View File

@ -1,66 +0,0 @@
import { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Menu from './Menu';
import useDocumentClick from 'hooks/useDocumentClick';
import Chevron from 'assets/chevron-down.svg';
import styles from './Dropdown.module.css';
import { Icon } from 'react-basics';
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
const [showMenu, setShowMenu] = useState(false);
const ref = useRef();
const selectedOption = options.find(e => e.value === value);
function handleShowMenu() {
setShowMenu(state => !state);
}
function handleSelect(selected, e) {
e.stopPropagation();
setShowMenu(false);
onChange(selected);
}
useDocumentClick(e => {
if (!ref.current?.contains(e.target)) {
setShowMenu(false);
}
});
return (
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
<div className={styles.value}>
<div className={styles.text}>{options.find(e => e.value === value)?.label || value}</div>
<Icon className={styles.icon} size="small">
<Chevron />
</Icon>
</div>
{showMenu && (
<Menu
className={menuClassName}
options={options}
selectedOption={selectedOption}
onSelect={handleSelect}
float="bottom"
/>
)}
</div>
);
}
DropDown.propTypes = {
value: PropTypes.any,
className: PropTypes.string,
menuClassName: PropTypes.string,
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.any.isRequired,
label: PropTypes.node,
}),
),
onChange: PropTypes.func,
};
export default DropDown;

View File

@ -1,27 +0,0 @@
.dropdown {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--base500);
border-radius: 4px;
cursor: pointer;
}
.value {
flex: 1;
display: flex;
justify-content: space-between;
flex-wrap: nowrap;
white-space: nowrap;
padding: 4px 16px;
min-width: 160px;
}
.text {
flex: 1;
}
.icon {
padding-left: 20px;
}

View File

@ -2,8 +2,7 @@ import classNames from 'classnames';
import Link from 'next/link';
import { safeDecodeURI } from 'next-basics';
import usePageQuery from 'hooks/usePageQuery';
import External from 'assets/arrow-up-right-from-square.svg';
import { Icon } from 'react-basics';
import { Icon, Icons } from 'react-basics';
import styles from './FilterLink.module.css';
export default function FilterLink({ id, value, label, externalUrl }) {
@ -26,7 +25,7 @@ export default function FilterLink({ id, value, label, externalUrl }) {
{externalUrl && (
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
<Icon className={styles.icon}>
<External />
<Icons.External />
</Icon>
</a>
)}

View File

@ -1,10 +1,9 @@
import { Button, Icon } from 'react-basics';
import XMark from 'assets/xmark.svg';
import Bars from 'assets/bars.svg';
import { useState } from 'react';
import styles from './HamburgerButton.module.css';
import MobileMenu from './MobileMenu';
import { FormattedMessage } from 'react-intl';
import MobileMenu from './MobileMenu';
import Icons from 'components/icons';
import styles from './HamburgerButton.module.css';
const menuItems = [
{
@ -37,7 +36,7 @@ export default function HamburgerButton() {
return (
<>
<Button className={styles.button} onClick={handleClick}>
<Icon>{active ? <XMark /> : <Bars />}</Icon>
<Icon>{active ? <Icons.Close /> : <Icons.Menu />}</Icon>
</Button>
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
</>

View File

@ -1,19 +1,25 @@
import classNames from 'classnames';
import Link from './Link';
import { Button } from 'react-basics';
import XMark from 'assets/xmark.svg';
import Link from 'next/link';
import { Button, Icon } from 'react-basics';
import Icons from 'components/icons';
import styles from './MobileMenu.module.css';
export default function MobileMenu({ items = [], onClose }) {
return (
<div className={classNames(styles.menu, 'container')}>
<div className={classNames(styles.menu)}>
<div className={styles.header}>
<Button icon={<XMark />} onClick={onClose} />
<Button onClick={onClose}>
<Icon>
<Icons.Close />
</Icon>
</Button>
</div>
<div className={styles.items}>
{items.map(({ label, value }) => (
<Link key={value} href={value} className={styles.item} onClick={onClose}>
{label}
<Link key={value} href={value}>
<a className={styles.item} onClick={onClose}>
{label}
</a>
</Link>
))}
</div>

View File

@ -1,12 +1,10 @@
import { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import useStore from 'store/queries';
import { setDateRange } from 'store/websites';
import { Button, Icon } from 'react-basics';
import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg';
import useDateRange from 'hooks/useDateRange';
import Icons from 'components/icons';
function RefreshButton({ websiteId }) {
const [dateRange] = useDateRange(websiteId);
@ -17,7 +15,7 @@ function RefreshButton({ websiteId }) {
function handleClick() {
if (!loading && dateRange) {
setLoading(true);
if (/^[\d]+/.test(dateRange.value)) {
if (/^\d+/.test(dateRange.value)) {
setDateRange(websiteId, dateRange.value);
} else {
setDateRange(websiteId, dateRange);
@ -36,13 +34,11 @@ function RefreshButton({ websiteId }) {
size="small"
onClick={handleClick}
>
<Icon>{loading ? <Dots /> : <Refresh />}</Icon>
<Icon>
<Icons.Refresh />
</Icon>
</Button>
);
}
RefreshButton.propTypes = {
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default RefreshButton;

View File

@ -1,3 +1,4 @@
import { Icons } from 'react-basics';
import Bolt from 'assets/bolt.svg';
import Calendar from 'assets/calendar.svg';
import Clock from 'assets/clock.svg';
@ -12,7 +13,8 @@ import Sun from 'assets/sun.svg';
import User from 'assets/user.svg';
import Users from 'assets/users.svg';
export {
const icons = {
...Icons,
Bolt,
Calendar,
Clock,
@ -27,3 +29,5 @@ export {
User,
Users,
};
export default icons;

View File

@ -5,7 +5,11 @@ import useRequireLogin from 'hooks/useRequireLogin';
import styles from './AppLayout.module.css';
export default function AppLayout({ title, children }) {
useRequireLogin();
const { user } = useRequireLogin();
if (!user) {
return null;
}
return (
<div className={styles.layout}>

View File

@ -12,7 +12,7 @@ import styles from './Header.module.css';
import classNames from 'classnames';
export default function Header({ className }) {
const user = useUser();
const { user } = useUser();
const { pathname } = useRouter();
const { updatesDisabled, adminDisabled } = useConfig();
const isSharePage = pathname.includes('/share/');

View File

@ -1,32 +1,38 @@
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { Icon, Text, Icons } from 'react-basics';
import { Icon, Text } from 'react-basics';
import classNames from 'classnames';
import { Dashboard, Logo, Profile, User, Users, Clock, Globe } from 'components/icons';
import ThemeButton from '../buttons/ThemeButton';
import Icons from 'components/icons';
import ThemeButton from 'components/buttons/ThemeButton';
import LanguageButton from 'components/buttons/LanguageButton';
import LogoutButton from 'components/buttons/LogoutButton';
import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
import NavGroup from './NavGroup';
import styles from './NavBar.module.css';
export default function NavBar() {
const { user } = useUser();
const { formatMessage } = useIntl();
const [minimized, setMinimized] = useState(false);
const tooltipPosition = minimized ? 'right' : 'top';
const analytics = [
{ label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Dashboard /> },
{ label: formatMessage(labels.realtime), url: '/realtime', icon: <Clock /> },
{ label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Icons.Dashboard /> },
{ label: formatMessage(labels.realtime), url: '/realtime', icon: <Icons.Clock /> },
{ label: formatMessage(labels.queries), url: '/queries', icon: <Icons.Search /> },
];
const settings = [
{ label: formatMessage(labels.websites), url: '/settings/websites', icon: <Globe /> },
{ label: formatMessage(labels.users), url: '/settings/users', icon: <User /> },
{ label: formatMessage(labels.teams), url: '/settings/teams', icon: <Users /> },
{ label: formatMessage(labels.profile), url: '/settings/profile', icon: <Profile /> },
];
{ label: formatMessage(labels.websites), url: '/settings/websites', icon: <Icons.Globe /> },
user?.isAdmin && {
label: formatMessage(labels.users),
url: '/settings/users',
icon: <Icons.User />,
},
{ label: formatMessage(labels.teams), url: '/settings/teams', icon: <Icons.Users /> },
{ label: formatMessage(labels.profile), url: '/settings/profile', icon: <Icons.Profile /> },
].filter(n => n);
const handleMinimize = () => setMinimized(state => !state);
@ -34,7 +40,7 @@ export default function NavBar() {
<div className={classNames(styles.navbar, { [styles.minimized]: minimized })}>
<div className={styles.header} onClick={handleMinimize}>
<Icon size="lg">
<Logo />
<Icons.Logo />
</Icon>
<Text className={styles.text}>umami</Text>
<Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}>

View File

@ -1,24 +1,11 @@
import React from 'react';
import Link from 'next/link';
import classNames from 'classnames';
import { Button, Icon } from 'react-basics';
import styles from './PageHeader.module.css';
export default function PageHeader({ title, backUrl, children, className, style }) {
export default function PageHeader({ title, children, className, style }) {
return (
<div className={classNames(styles.header, className)} style={style}>
<div className={styles.title}>
{backUrl && (
<Link href={backUrl}>
<a>
<Button>
<Icon icon="arrow-left" /> Back
</Button>
</a>
</Link>
)}
{title}
</div>
<div className={styles.title}>{title}</div>
{children}
</div>
);

View File

@ -5,8 +5,6 @@
align-content: center;
align-self: stretch;
margin-bottom: 40px;
font-size: 18px;
font-weight: bold;
height: 50px;
}
@ -21,5 +19,7 @@
.title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
gap: 20px;
}

View File

@ -48,7 +48,7 @@ export const labels = defineMessages({
newPassword: { id: 'label.new-password', defaultMessage: 'New password' },
confirmPassword: { id: 'label.confirm-password', defaultMessage: 'Confirm password' },
timezone: { id: 'label.timezone', defaultMessage: 'Timezone' },
dateRange: { id: 'label.default-date-range', defaultMessage: 'Default date range' },
defaultDateRange: { id: 'label.default-date-range', defaultMessage: 'Default date range' },
language: { id: 'label.language', defaultMessage: 'Language' },
theme: { id: 'label.theme', defaultMessage: 'Theme' },
profile: { id: 'label.profile', defaultMessage: 'Profile' },
@ -59,6 +59,8 @@ export const labels = defineMessages({
teams: { id: 'label.teams', defaultMessage: 'Teams' },
analytics: { id: 'label.analytics', defaultMessage: 'Analytics' },
logout: { id: 'label.logout', defaultMessage: 'Logout' },
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
});
export const messages = defineMessages({

View File

@ -1,14 +1,15 @@
import Calendar from 'components/common/Calendar';
import { FormButtons } from 'components/layout/FormLayout';
import { isAfter, isBefore, isSameDay } from 'date-fns';
import { getDateRangeValues } from 'lib/date';
import { useState } from 'react';
import { Button, ButtonGroup } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import { Button, ButtonGroup, Calendar } from 'react-basics';
import { useIntl } from 'react-intl';
import { isAfter, isBefore, isSameDay } from 'date-fns';
import useLocale from 'hooks/useLocale';
import { getDateRangeValues } from 'lib/date';
import { getDateLocale } from 'lib/lang';
import { labels } from 'components/messages';
import styles from './DatePickerForm.module.css';
const FILTER_DAY = 0;
const FILTER_RANGE = 1;
const FILTER_DAY = 'day';
const FILTER_RANGE = 'range';
export default function DatePickerForm({
startDate: defaultStartDate,
@ -24,59 +25,59 @@ export default function DatePickerForm({
const [date, setDate] = useState(defaultStartDate);
const [startDate, setStartDate] = useState(defaultStartDate);
const [endDate, setEndDate] = useState(defaultEndDate);
const { locale } = useLocale();
const { formatMessage } = useIntl();
const disabled =
selected === FILTER_DAY
? isAfter(minDate, date) && isBefore(maxDate, date)
: isAfter(startDate, endDate);
const buttons = [
{
label: <FormattedMessage id="label.single-day" defaultMessage="Single day" />,
value: FILTER_DAY,
},
{
label: <FormattedMessage id="label.date-range" defaultMessage="Date range" />,
value: FILTER_RANGE,
},
];
function handleSave() {
const handleSave = () => {
if (selected === FILTER_DAY) {
onChange({ ...getDateRangeValues(date, date), value: 'custom' });
} else {
onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' });
}
}
};
return (
<div className={styles.container}>
<div className={styles.filter}>
<ButtonGroup size="small" items={buttons} selectedItem={selected} onClick={setSelected} />
<ButtonGroup size="sm" selectedKey={selected} onSelect={setSelected}>
<Button key={FILTER_DAY}>{formatMessage(labels.singleDay)}</Button>
<Button key={FILTER_RANGE}>{formatMessage(labels.dateRange)}</Button>
</ButtonGroup>
</div>
<div className={styles.calendars}>
{selected === FILTER_DAY ? (
{selected === FILTER_DAY && (
<Calendar date={date} minDate={minDate} maxDate={maxDate} onChange={setDate} />
) : (
)}
{selected === FILTER_RANGE && (
<>
<Calendar
date={startDate}
minDate={minDate}
maxDate={endDate}
locale={getDateLocale(locale)}
onChange={setStartDate}
/>
<Calendar date={endDate} minDate={startDate} maxDate={maxDate} onChange={setEndDate} />
<Calendar
date={endDate}
minDate={startDate}
maxDate={maxDate}
locale={getDateLocale(locale)}
onChange={setEndDate}
/>
</>
)}
</div>
<FormButtons>
<Button variant="action" onClick={handleSave} disabled={disabled}>
<FormattedMessage id="label.save" defaultMessage="Save" />
<div className={styles.buttons}>
<Button variant="primary" onClick={handleSave} disabled={disabled}>
{formatMessage(labels.save)}
</Button>
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</div>
</div>
);
}

View File

@ -26,6 +26,14 @@
margin-bottom: 20px;
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
@media only screen and (max-width: 768px) {
.calendars {
flex-direction: column;

View File

@ -1,11 +1,10 @@
import { useMemo } from 'react';
import { Loading } from 'react-basics';
import { Loading, Icons } from 'react-basics';
import { defineMessages, useIntl } from 'react-intl';
import firstBy from 'thenby';
import classNames from 'classnames';
import Link from 'components/common/Link';
import useApi from 'hooks/useApi';
import Arrow from 'assets/arrow-right.svg';
import { percentFilter } from 'lib/filters';
import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery';
@ -80,7 +79,7 @@ export default function MetricsTable({
<div className={styles.footer}>
{data && !error && limit && (
<Link
icon={<Arrow />}
icon={<Icons.ArrowRight />}
href={router.pathname}
as={resolve({ view: type })}
size="small"

View File

@ -2,25 +2,11 @@ import { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import { differenceInMinutes } from 'date-fns';
import PageHeader from 'components/layout/PageHeader';
import DropDown from 'components/common/DropDown';
import ActiveUsers from './ActiveUsers';
import MetricCard from './MetricCard';
import styles from './RealtimeHeader.module.css';
export default function RealtimeHeader({ websites, data, websiteId, onSelect }) {
const options = [
{
label: <FormattedMessage id="label.all-websites" defaultMessage="All websites" />,
value: null,
},
].concat(
websites.map(({ name, id }, index) => ({
label: name,
value: id,
divider: index === 0,
})),
);
export default function RealtimeHeader({ data, websiteId }) {
const { pageviews, sessions, events, countries } = data;
const count = useMemo(() => {
@ -38,7 +24,6 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
<div>
<ActiveUsers className={styles.active} value={count} />
</div>
<DropDown value={websiteId} options={options} onChange={onSelect} />
</PageHeader>
<div className={styles.metrics}>
<MetricCard

View File

@ -1,8 +1,7 @@
import { Button, Column, Loading, Row } from 'react-basics';
import { Button, Column, Row, Dropdown, Item } from 'react-basics';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import DropDown from 'components/common/DropDown';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import EventsChart from 'components/metrics/EventsChart';
@ -12,29 +11,14 @@ import styles from './TestConsole.module.css';
export default function TestConsole() {
const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['websites:test-console'], () =>
get('/websites?include_all=true'),
);
const { data, isLoading, error } = useQuery(['websites:test-console'], () => get('/websites'));
const router = useRouter();
const {
basePath,
query: { id },
} = router;
const websiteId = id?.[0];
if (isLoading) {
return <Loading />;
}
if (!data) {
return null;
}
const options = data.map(({ name, id }) => ({ label: name, value: id }));
const website = data.find(({ id }) => websiteId === id);
const selectedValue = options.find(({ value }) => value === website?.id)?.value;
function handleSelect(value) {
function handleChange(value) {
router.push(`/console/${value}`);
}
@ -45,8 +29,15 @@ export default function TestConsole() {
window.umami.trackEvent('track-event-with-data', { test: 'test-data', time: Date.now() });
}
if (!data) {
return null;
}
const websiteId = id?.[0];
const website = data.find(({ id }) => websiteId === id);
return (
<Page>
<Page loading={isLoading} error={error}>
<Head>
{typeof window !== 'undefined' && website && (
<script
@ -58,19 +49,22 @@ export default function TestConsole() {
/>
)}
</Head>
<PageHeader>
<div>Test Console</div>
<DropDown
value={selectedValue || 'Select website'}
options={options}
onChange={handleSelect}
/>
<PageHeader title="Test console">
<Dropdown
items={data}
renderValue={() => website?.name || 'Select website'}
value={website?.id}
onChange={handleChange}
style={{ width: 300 }}
>
{({ id, name }) => <Item key={id}>{name}</Item>}
</Dropdown>
</PageHeader>
{website && (
<>
<Row className={styles.test}>
<Column xs="4">
<PageHeader>Page links</PageHeader>
<div className={styles.header}>Page links</div>
<div>
<Link href={`/console/${websiteId}?page=1`}>
<a>page one</a>
@ -95,13 +89,13 @@ export default function TestConsole() {
</div>
</Column>
<Column xs="4">
<PageHeader>CSS events</PageHeader>
<div className={styles.header}>CSS events</div>
<Button id="primary-button" className="umami--click--button-click" variant="action">
Send event
</Button>
</Column>
<Column xs="4">
<PageHeader>Javascript events</PageHeader>
<div className={styles.header}>Javascript events</div>
<Button id="manual-button" variant="action" onClick={handleClick}>
Run script
</Button>
@ -109,13 +103,14 @@ export default function TestConsole() {
</Row>
<Row>
<Column>
<div className={styles.header}>Statistics</div>
<WebsiteChart
websiteId={website.id}
title={website.name}
domain={website.domain}
showLink
/>
<PageHeader>Events</PageHeader>
<div className={styles.header}>Events</div>
<EventsChart websiteId={website.id} />
</Column>
</Row>

View File

@ -4,6 +4,12 @@
padding: 0 20px 20px 20px;
}
.header {
font-size: 16px;
font-weight: 700;
line-height: 90px;
}
.hidden {
transform: rotate(-90deg);
}

View File

@ -1,7 +1,7 @@
import { useIntl } from 'react-intl';
import { Button, Icon, Text, useToast, ModalTrigger, Modal } from 'react-basics';
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
import { Lock } from 'components/icons';
import Icons from 'components/icons';
import { labels, messages } from 'components/messages';
export default function PasswordChangeButton() {
@ -18,7 +18,7 @@ export default function PasswordChangeButton() {
<ModalTrigger modalProps={{ title: formatMessage(labels.changePassword) }}>
<Button>
<Icon>
<Lock />
<Icons.Lock />
</Icon>
<Text>{formatMessage(labels.changePassword)}</Text>
</Button>

View File

@ -21,15 +21,15 @@ export default function ProfileDetails() {
<Form>
<FormRow label={formatMessage(labels.username)}>{username}</FormRow>
<FormRow label={formatMessage(labels.role)}>{role}</FormRow>
<FormRow label={formatMessage(labels.defaultDateRange)}>
<DateRangeSetting />
</FormRow>
<FormRow label={formatMessage(labels.language)}>
<LanguageSetting />
</FormRow>
<FormRow label={formatMessage(labels.timezone)}>
<TimezoneSetting />
</FormRow>
<FormRow label={formatMessage(labels.dateRange)}>
<DateRangeSetting />
</FormRow>
<FormRow label={formatMessage(labels.theme)}>
<ThemeSetting />
</FormRow>

View File

@ -42,7 +42,7 @@ export default function TeamEditForm({ teamId, data, onSave }) {
};
return (
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data} style={{ width: 600 }}>
<FormRow label={formatMessage(labels.teamId)}>
<TextField value={teamId} readOnly allowCopy />
</FormRow>

View File

@ -14,14 +14,16 @@ import {
import { useIntl } from 'react-intl';
import { ROLES } from 'lib/constants';
import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
export default function TeamMembersTable({ data = [] }) {
const { formatMessage } = useIntl();
const { user } = useUser();
const columns = [
{ name: 'username', label: formatMessage(labels.username), style: { flex: 4 } },
{ name: 'role', label: formatMessage(labels.role) },
{ name: 'action', label: '' },
{ name: 'username', label: formatMessage(labels.username), style: { flex: 2 } },
{ name: 'role', label: formatMessage(labels.role), style: { flex: 1 } },
{ name: 'action', label: '', style: { flex: 1 } },
];
return (
@ -43,14 +45,14 @@ export default function TeamMembersTable({ data = [] }) {
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
),
action: (
<div>
<Button>
<Flexbox flex={1} justifyContent="end">
<Button disabled={user.id === row?.user?.id}>
<Icon>
<Icons.Close />
</Icon>
<Text>{formatMessage(labels.remove)}</Text>
</Button>
</div>
</Flexbox>
),
};
@ -59,11 +61,7 @@ export default function TeamMembersTable({ data = [] }) {
{(data, key, colIndex) => {
return (
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
<Flexbox
flex={1}
alignItems="center"
justifyContent={key === 'action' ? 'end' : undefined}
>
<Flexbox flex={1} alignItems="center">
{data[key]}
</Flexbox>
</TableCell>

View File

@ -7,23 +7,20 @@ import {
TableCell,
TableColumn,
Button,
Text,
Icon,
Icons,
Flexbox,
} from 'react-basics';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
name: { id: 'label.name', defaultMessage: 'Name' },
domain: { id: 'label.domain', defaultMessage: 'Domain' },
});
import { useIntl } from 'react-intl';
import { labels } from 'components/messages';
export default function WebsitesTable({ data = [] }) {
const { formatMessage } = useIntl();
const columns = [
{ name: 'name', label: formatMessage(messages.name), style: { flex: 2 } },
{ name: 'domain', label: formatMessage(messages.domain) },
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
{ name: 'domain', label: formatMessage(labels.domain) },
{ name: 'action', label: ' ' },
];
@ -50,7 +47,7 @@ export default function WebsitesTable({ data = [] }) {
<Icon>
<Icons.ArrowRight />
</Icon>
Settings
<Text>Settings</Text>
</Button>
</a>
</Link>
@ -60,7 +57,7 @@ export default function WebsitesTable({ data = [] }) {
<Icon>
<Icons.External />
</Icon>
View
<Text>View</Text>
</Button>
</a>
</Link>

View File

@ -1,4 +1,4 @@
import Arrow from 'assets/arrow-right.svg';
import { Icons } from 'react-basics';
import classNames from 'classnames';
import Link from 'components/common/Link';
import WorldMap from 'components/common/WorldMap';
@ -67,7 +67,12 @@ export default function WebsiteDetails({ websiteId }) {
const BackButton = () => (
<div key="back-button" className={classNames(styles.backButton, 'col-12')}>
<Link key="back-button" href={resolve({ view: undefined })} icon={<Arrow />} sizes="small">
<Link
key="back-button"
href={resolve({ view: undefined })}
icon={<Icons.ArrowRight />}
sizes="small"
>
<FormattedMessage id="label.back" defaultMessage="Back" />
</Link>
</div>

View File

@ -11,10 +11,12 @@ import { getUser } from '../queries';
const log = debug('umami:middleware');
export const useCors = createMiddleware(cors({
// Cache CORS preflight request 24 hours by default
maxAge: process.env.CORS_MAX_AGE || 86400,
}));
export const useCors = createMiddleware(
cors({
// Cache CORS preflight request 24 hours by default
maxAge: process.env.CORS_MAX_AGE || 86400,
}),
);
export const useSession = createMiddleware(async (req, res, next) => {
const session = await findSession(req);
@ -34,17 +36,17 @@ export const useAuth = createMiddleware(async (req, res, next) => {
const shareToken = await parseShareToken(req);
let user = null;
const { userId, key } = payload || {};
const { userId, authKey } = payload || {};
if (validate(userId)) {
user = await getUser({ id: userId });
} else if (redis.enabled && key) {
user = await redis.get(key);
} else if (redis.enabled && authKey) {
user = await redis.get(authKey);
}
log({ token, payload, user, shareToken });
if (!user && !shareToken) {
if (!user?.id && !shareToken) {
log('useAuth: User not authorized');
return unauthorized(res);
}
@ -53,6 +55,6 @@ export const useAuth = createMiddleware(async (req, res, next) => {
user.isAdmin = user.role === ROLES.admin;
}
(req as any).auth = { user, token, shareToken, key };
(req as any).auth = { user, token, shareToken, authKey };
next();
});

View File

@ -1,6 +1,6 @@
{
"name": "umami",
"version": "2.0.0-beta.2",
"version": "2.0.0-beta.3",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
@ -94,7 +94,7 @@
"npm-run-all": "^4.1.5",
"prop-types": "^15.7.2",
"react": "^18.2.0",
"react-basics": "^0.63.0",
"react-basics": "^0.64.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-intl": "^5.24.7",

View File

@ -1,11 +1,8 @@
import AppLayout from 'components/layout/AppLayout';
import TestConsole from 'components/pages/console/TestConsole';
import useUser from 'hooks/useUser';
export default function ConsolePage({ pageDisabled }) {
const { user } = useUser();
if (pageDisabled || !user || !user.isAdmin) {
if (pageDisabled) {
return null;
}

View File

@ -4,7 +4,7 @@ import useUser from 'hooks/useUser';
import { useRouter } from 'next/router';
export default function TeamDetailPage() {
const user = useUser();
const { user } = useUser();
const router = useRouter();
const { id } = router.query;

View File

@ -3,7 +3,7 @@ import TeamsList from 'components/pages/settings/teams/TeamsList';
import useUser from 'hooks/useUser';
export default function TeamsPage() {
const user = useUser();
const { user } = useUser();
if (!user) {
return null;

View File

@ -4,7 +4,7 @@ import useUser from 'hooks/useUser';
import { useRouter } from 'next/router';
export default function TeamDetailPage() {
const user = useUser();
const { user } = useUser();
const router = useRouter();
const { id } = router.query;

View File

@ -5,7 +5,7 @@ import useUser from 'hooks/useUser';
import UsersList from 'components/pages/settings/users/UsersList';
export default function UsersPage() {
const user = useUser();
const { user } = useUser();
const { adminDisabled } = useConfig();
if (adminDisabled || !user) {

View File

@ -1,14 +1,12 @@
import { useRouter } from 'next/router';
import WebsiteSettings from 'components/pages/settings/websites/WebsiteSettings';
import useUser from 'hooks/useUser';
import AppLayout from 'components/layout/AppLayout';
export default function WebsiteSettingsPage() {
const user = useUser();
const router = useRouter();
const { id } = router.query;
if (!id || !user) {
if (!id) {
return null;
}

View File

@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { Loading } from 'react-basics';
import { useRouter } from 'next/router';
import { setClientAuthToken } from 'lib/client';
@ -14,5 +15,5 @@ export default function SingleSignOnPage() {
}
}, [router, url, token]);
return null;
return <Loading size="xl" />;
}

View File

@ -6586,10 +6586,10 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-basics@^0.63.0:
version "0.63.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.63.0.tgz#1b203c701f6936076633994ed8175234bde93694"
integrity sha512-G6+1Z921kC/TyjZCABrDlNeB22YkN6q7V70xREGSiO55OXU73MLT+Kk96GDWYfKa5lB1zI5rCbbvGz3ELuU+mA==
react-basics@^0.64.0:
version "0.64.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.64.0.tgz#b921dab7e437db6655f033cae15b8c963b93b7b2"
integrity sha512-MY/F5+VBqqi+Hx58PdRONoeu3W0sitPOFbvAGxiM9vpajQL1DD//0Xgl/MahW8sIDbMy00lFghouex5JS93C8Q==
dependencies:
classnames "^2.3.1"
date-fns "^2.29.3"