mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 09:45:04 +01:00
More refactoring.
This commit is contained in:
parent
5f15ad0807
commit
02a1438cfe
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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>
|
||||
)}
|
||||
|
@ -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} />}
|
||||
</>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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}>
|
||||
|
@ -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/');
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -4,6 +4,12 @@
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 90px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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" />;
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user