Merge branch 'dev' into bh/unregister

This commit is contained in:
Bartosz Hernas 2020-09-18 11:33:30 +02:00
commit 17b4f51ebe
78 changed files with 799 additions and 548 deletions

View file

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart';
import WorldMap from 'components/common/WorldMap';
@ -19,12 +20,29 @@ import EventsChart from './metrics/EventsChart';
import useFetch from 'hooks/useFetch';
import Loading from 'components/common/Loading';
export default function WebsiteDetails({ websiteId }) {
const { data } = useFetch(`/api/website/${websiteId}`);
const views = {
url: PagesTable,
referrer: ReferrersTable,
browser: BrowsersTable,
os: OSTable,
device: DevicesTable,
country: CountriesTable,
event: EventsTable,
};
export default function WebsiteDetails({ websiteId, token }) {
const router = useRouter();
const { data } = useFetch(`/api/website/${websiteId}`, { token });
const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState();
const [expand, setExpand] = useState();
const {
query: { id, view },
basePath,
asPath,
} = router;
const path = `${basePath}/${asPath.split('/')[1]}/${id.join('/')}`;
const BackButton = () => (
<Button
@ -32,7 +50,7 @@ export default function WebsiteDetails({ websiteId }) {
className={styles.backButton}
icon={<Arrow />}
size="xsmall"
onClick={() => setExpand(null)}
onClick={() => router.push(path)}
>
<div>
<FormattedMessage id="button.back" defaultMessage="Back" />
@ -46,53 +64,43 @@ export default function WebsiteDetails({ websiteId }) {
},
{
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
value: 'url',
component: PagesTable,
value: `${path}?view=url`,
},
{
label: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />,
value: 'referrer',
component: ReferrersTable,
value: `${path}?view=referrer`,
},
{
label: <FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />,
value: 'browser',
component: BrowsersTable,
value: `${path}?view=browser`,
},
{
label: <FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />,
value: 'os',
component: OSTable,
value: `${path}?view=os`,
},
{
label: <FormattedMessage id="metrics.devices" defaultMessage="Devices" />,
value: 'device',
component: DevicesTable,
value: `${path}?view=device`,
},
{
label: <FormattedMessage id="metrics.countries" defaultMessage="Countries" />,
value: 'country',
component: CountriesTable,
value: `${path}?view=country`,
},
{
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
value: 'event',
component: EventsTable,
value: `${path}?view=event`,
},
];
const tableProps = {
websiteId,
token,
websiteDomain: data?.domain,
limit: 10,
onExpand: handleExpand,
};
const DetailsComponent = expand?.component;
function getSelectedMenuOption(value) {
return menuOptions.find(e => e.value === value);
}
const DetailsComponent = views[view];
function handleDataLoad() {
if (!chartLoaded) {
@ -101,11 +109,7 @@ export default function WebsiteDetails({ websiteId }) {
}
function handleExpand(value) {
setExpand(getSelectedMenuOption(value));
}
function handleMenuSelect(value) {
setExpand(getSelectedMenuOption(value));
router.push(`${path}?view=${value}`);
}
if (!data) {
@ -118,6 +122,7 @@ export default function WebsiteDetails({ websiteId }) {
<div className={classNames(styles.chart, 'col')}>
<WebsiteChart
websiteId={websiteId}
token={token}
title={data.name}
onDataLoad={handleDataLoad}
showLink={false}
@ -126,7 +131,7 @@ export default function WebsiteDetails({ websiteId }) {
</div>
</div>
{!chartLoaded && <Loading />}
{chartLoaded && !expand && (
{chartLoaded && !view && (
<>
<div className={classNames(styles.row, 'row')}>
<div className="col-md-12 col-lg-6">
@ -162,19 +167,17 @@ export default function WebsiteDetails({ websiteId }) {
<EventsTable {...tableProps} onDataLoad={setEventsData} />
</div>
<div className="col-12 col-md-12 col-lg-8 pt-5 pb-5">
<EventsChart websiteId={websiteId} />
<EventsChart websiteId={websiteId} token={token} />
</div>
</div>
</>
)}
{expand && (
{view && (
<MenuLayout
className={styles.expand}
className={styles.view}
menuClassName={styles.menu}
optionClassName={styles.option}
contentClassName={styles.content}
menu={menuOptions}
selectedOption={expand.value}
onMenuSelect={handleMenuSelect}
>
<DetailsComponent {...tableProps} limit={false} />
</MenuLayout>

View file

@ -2,7 +2,7 @@
margin-bottom: 30px;
}
.expand {
.view {
border-top: 1px solid var(--gray300);
}
@ -10,8 +10,8 @@
font-size: var(--font-size-small);
}
.menu .option {
font-size: var(--font-size-small);
.content {
min-height: 600px;
}
.backButton {

View file

@ -11,7 +11,7 @@ import styles from './WebsiteList.module.css';
export default function WebsiteList({ userId }) {
const router = useRouter();
const { data } = useFetch('/api/websites', { userId });
const { data } = useFetch('/api/websites', { user_id: userId });
if (!data) {
return null;
@ -28,17 +28,14 @@ export default function WebsiteList({ userId }) {
<EmptyPlaceholder
msg={
<FormattedMessage
id="placeholder.message.no-websites-configured"
id="message.no-websites-configured"
defaultMessage="You don't have any websites configured."
/>
}
>
<Button icon={<Arrow />} size="medium" onClick={() => router.push('/settings')}>
<div>
<FormattedMessage
id="placeholder.message.go-to-settings"
defaultMessage="Go to settings"
/>
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
</div>
</Button>
</EmptyPlaceholder>

View file

@ -70,34 +70,36 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
<Icon className={styles.icon} icon={selectYear ? <Cross /> : <Chevron />} size="small" />
</div>
</div>
{!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 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>
);
}
@ -220,42 +222,46 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
return (
<div className={styles.pager}>
<Button
icon={<Chevron />}
size="small"
className={styles.left}
onClick={handlePrevClick}
disabled={years[0] <= minYear}
variant="light"
/>
<table>
<tbody>
{chunk(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>
<Button
icon={<Chevron />}
size="small"
className={styles.right}
onClick={handleNextClick}
disabled={years[years.length - 1] > maxYear}
variant="light"
/>
<div className={styles.left}>
<Button
icon={<Chevron />}
size="small"
onClick={handlePrevClick}
disabled={years[0] <= minYear}
variant="light"
/>
</div>
<div className={styles.middle}>
<table>
<tbody>
{chunk(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
icon={<Chevron />}
size="small"
onClick={handleNextClick}
disabled={years[years.length - 1] > maxYear}
variant="light"
/>
</div>
</div>
);
};

View file

@ -3,11 +3,11 @@
flex-direction: column;
font-size: var(--font-size-small);
flex: 1;
min-height: 285px;
min-height: 306px;
}
.calendar table {
flex: 1;
width: 100%;
border-spacing: 5px;
}
@ -64,18 +64,34 @@
font-size: var(--font-size-normal);
}
.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);
}

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { endOfYear } from 'date-fns';
import { endOfYear, isSameDay } from 'date-fns';
import Modal from './Modal';
import DropDown from './DropDown';
import DatePickerForm from 'components/forms/DatePickerForm';
@ -112,7 +112,8 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
return (
<>
<Icon icon={<Calendar />} className="mr-2" onClick={handleClick} />
{`${dateFormat(startDate, 'd LLL y', locale)}${dateFormat(endDate, 'd LLL y', locale)}`}
{dateFormat(startDate, 'd LLL y', locale)}
{!isSameDay(startDate, endDate) && `${dateFormat(endDate, 'd LLL y', locale)}`}
</>
);
};

View file

@ -1,12 +1,13 @@
import React, { useState, useRef } from 'react';
import Head from 'next/head';
import Globe from 'assets/globe.svg';
import useDocumentClick from 'hooks/useDocumentClick';
import Menu from './Menu';
import Button from './Button';
import { menuOptions } from 'lib/lang';
import { setItem } from 'lib/web';
import useLocale from 'hooks/useLocale';
import useDocumentClick from 'hooks/useDocumentClick';
import Globe from 'assets/globe.svg';
import styles from './LanguageButton.module.css';
import useLocale from '../../hooks/useLocale';
export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'left' }) {
const [showMenu, setShowMenu] = useState(false);
@ -16,7 +17,7 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l
function handleSelect(value) {
setLocale(value);
window.localStorage.setItem('locale', value);
setItem('umami.locale', value);
setShowMenu(false);
}

View file

@ -0,0 +1,32 @@
import React from 'react';
import { useRouter } from 'next/router';
import classNames from 'classnames';
import styles from './NavMenu.module.css';
export default function NavMenu({ options = [], className, onSelect = () => {} }) {
const router = useRouter();
return (
<div className={classNames(styles.menu, className)}>
{options
.filter(({ hidden }) => !hidden)
.map(option => {
const { label, value, className: customClassName, render } = option;
return render ? (
render(option)
) : (
<div
key={value}
className={classNames(styles.option, customClassName, {
[styles.selected]: router.asPath === value,
})}
onClick={e => onSelect(value, e)}
>
{label}
</div>
);
})}
</div>
);
}

View file

@ -0,0 +1,20 @@
.menu {
border: 1px solid var(--gray500);
border-radius: 4px;
overflow: hidden;
z-index: 2;
}
.option {
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
}
.option:hover {
background: var(--gray75);
}
.selected {
font-weight: 600;
}

View file

@ -5,7 +5,7 @@ import { setDateRange } from 'redux/actions/websites';
import Button from './Button';
import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
import { getDateRange } from '../../lib/date';
export default function RefreshButton({ websiteId }) {

View file

@ -27,6 +27,7 @@ export default function UserButton() {
value: 'username',
className: styles.username,
},
{ label: <FormattedMessage id="label.profile" defaultMessage="Profile" />, value: 'profile' },
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
];
@ -35,6 +36,8 @@ export default function UserButton() {
if (value === 'logout') {
router.push('/logout');
} else if (value === 'profile') {
router.push('/settings/profile');
}
}

View file

@ -1,11 +1,15 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { isAfter } from 'date-fns';
import { isAfter, isBefore, isSameDay } from 'date-fns';
import Calendar from 'components/common/Calendar';
import Button from 'components/common/Button';
import { FormButtons } from 'components/layout/FormLayout';
import { getDateRangeValues } from 'lib/date';
import styles from './DatePickerForm.module.css';
import ButtonGroup from '../common/ButtonGroup';
const FILTER_DAY = 0;
const FILTER_RANGE = 1;
export default function DatePickerForm({
startDate: defaultStartDate,
@ -15,21 +19,59 @@ export default function DatePickerForm({
onChange,
onClose,
}) {
const [selected, setSelected] = useState(
isSameDay(defaultStartDate, defaultEndDate) ? FILTER_DAY : FILTER_RANGE,
);
const [date, setDate] = useState(defaultStartDate);
const [startDate, setStartDate] = useState(defaultStartDate);
const [endDate, setEndDate] = useState(defaultEndDate);
const disabled =
selected === FILTER_DAY
? isAfter(minDate, date) && isBefore(maxDate, date)
: isAfter(startDate, endDate);
const buttons = [
{
label: <FormattedMessage id="button.single-day" defaultMessage="Single day" />,
value: FILTER_DAY,
},
{
label: <FormattedMessage id="button.date-range" defaultMessage="Date range" />,
value: FILTER_RANGE,
},
];
function handleSave() {
onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' });
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} />
</div>
<div className={styles.calendars}>
<Calendar date={startDate} minDate={minDate} maxDate={endDate} onChange={setStartDate} />
<Calendar date={endDate} minDate={startDate} maxDate={maxDate} onChange={setEndDate} />
{selected === FILTER_DAY ? (
<Calendar date={date} minDate={minDate} maxDate={maxDate} onChange={setDate} />
) : (
<>
<Calendar
date={startDate}
minDate={minDate}
maxDate={endDate}
onChange={setStartDate}
/>
<Calendar date={endDate} minDate={startDate} maxDate={maxDate} onChange={setEndDate} />
</>
)}
</div>
<FormButtons>
<Button variant="action" onClick={handleSave} disabled={isAfter(startDate, endDate)}>
<Button variant="action" onClick={handleSave} disabled={disabled}>
<FormattedMessage id="button.save" defaultMessage="Save" />
</Button>
<Button onClick={onClose}>

View file

@ -1,25 +1,40 @@
.container {
display: flex;
flex-direction: column;
width: 800px;
max-width: 100vw;
}
.calendars {
display: flex;
justify-content: center;
}
.calendars > div:first-child {
padding-right: 20px;
border-right: 1px solid var(--gray300);
.calendars > div {
width: 380px;
}
.calendars > div:last-child {
.calendars > div + div {
margin-left: 20px;
padding-left: 20px;
border-left: 1px solid var(--gray300);
}
.filter {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
@media only screen and (max-width: 768px) {
.calendars {
flex-direction: column;
}
.calendars > div + div {
padding: 0;
margin-left: 0;
margin-top: 20px;
border: 0;
}
}

View file

@ -11,7 +11,7 @@ export default function Footer() {
<div />
<div>
<FormattedMessage
id="footer.powered-by"
id="message.powered-by"
defaultMessage="Powered by {name}"
values={{
name: (

View file

@ -26,10 +26,10 @@ export default function Header() {
{user ? (
<>
<Link href="/dashboard">
<FormattedMessage id="header.nav.dashboard" defaultMessage="Dashboard" />
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</Link>
<Link href="/settings">
<FormattedMessage id="header.nav.settings" defaultMessage="Settings" />
<FormattedMessage id="label.settings" defaultMessage="Settings" />
</Link>
<LanguageButton menuAlign="right" />
<UserButton />

View file

@ -1,29 +1,37 @@
import React from 'react';
import { useRouter } from 'next/router';
import classNames from 'classnames';
import Menu from 'components/common/Menu';
import NavMenu from 'components/common/NavMenu';
import styles from './MenuLayout.module.css';
export default function MenuLayout({
menu,
selectedOption,
onMenuSelect,
className,
menuClassName,
contentClassName,
optionClassName,
children,
replace = false,
}) {
const router = useRouter();
function handleSelect(url) {
if (replace) {
router.replace(url);
} else {
router.push(url);
}
}
return (
<div className={classNames(styles.container, className, 'row')}>
<Menu
<NavMenu
options={menu}
selectedOption={selectedOption}
className={classNames(styles.menu, menuClassName, 'col-12 col-lg-3')}
selectedClassName={styles.selected}
optionClassName={classNames(styles.option, optionClassName)}
onSelect={onMenuSelect}
className={classNames(styles.menu, menuClassName, 'col-12 col-lg-2')}
onSelect={handleSelect}
/>
<div className={classNames(styles.content, contentClassName, 'col-12 col-lg-9')}>
<div className={classNames(styles.content, contentClassName, 'col-12 col-lg-10')}>
{children}
</div>
</div>

View file

@ -10,25 +10,11 @@
}
.container .content {
flex: 1;
position: relative;
border-left: 1px solid var(--gray300);
padding-left: 30px;
}
.option {
font-size: var(--font-size-normal);
padding: 8px 16px;
cursor: pointer;
margin-right: 30px;
border-radius: 4px;
}
.option:hover {
background: var(--gray75);
}
.selected {
font-weight: 600;
margin-left: 30px;
}
@media only screen and (max-width: 992px) {
@ -40,5 +26,6 @@
border-top: 1px solid var(--gray300);
border-left: 0;
padding-left: 0;
margin-left: 0;
}
}

View file

@ -6,10 +6,10 @@ export const labels = defineMessages({
});
export const devices = defineMessages({
desktop: { id: 'device.desktop', defaultMessage: 'Desktop' },
laptop: { id: 'device.laptop', defaultMessage: 'Laptop' },
tablet: { id: 'device.tablet', defaultMessage: 'Tablet' },
mobile: { id: 'device.mobile', defaultMessage: 'Mobile' },
desktop: { id: 'metrics.device.desktop', defaultMessage: 'Desktop' },
laptop: { id: 'metrics.device.laptop', defaultMessage: 'Laptop' },
tablet: { id: 'metrics.device.tablet', defaultMessage: 'Tablet' },
mobile: { id: 'metrics.device.mobile', defaultMessage: 'Mobile' },
});
export function getDeviceMessage(device) {

View file

@ -4,8 +4,8 @@ import useFetch from 'hooks/useFetch';
import styles from './ActiveUsers.module.css';
import { FormattedMessage } from 'react-intl';
export default function ActiveUsers({ websiteId, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, {}, { interval: 60000 });
export default function ActiveUsers({ websiteId, token, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 });
const count = useMemo(() => {
return data?.[0]?.x || 0;
}, [data]);
@ -20,7 +20,7 @@ export default function ActiveUsers({ websiteId, className }) {
<div className={styles.text}>
<div>
<FormattedMessage
id="active-users.message"
id="message.active-users"
defaultMessage="{x} current {x, plural, one {visitor} other {visitors}}"
values={{ x: count }}
/>

View file

@ -3,13 +3,14 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import { browserFilter } from 'lib/filters';
export default function BrowsersTable({ websiteId, limit, onExpand }) {
export default function BrowsersTable({ websiteId, token, limit, onExpand }) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />}
type="browser"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={browserFilter}
onExpand={onExpand}

View file

@ -3,13 +3,20 @@ import MetricsTable from './MetricsTable';
import { countryFilter, percentFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function CountriesTable({ websiteId, limit, onDataLoad = () => {}, onExpand }) {
export default function CountriesTable({
websiteId,
token,
limit,
onDataLoad = () => {},
onExpand,
}) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
type="country"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={countryFilter}
onDataLoad={data => onDataLoad(percentFilter(data))}

View file

@ -4,13 +4,14 @@ import { deviceFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
import { getDeviceMessage } from 'components/messages';
export default function DevicesTable({ websiteId, limit, onExpand }) {
export default function DevicesTable({ websiteId, token, limit, onExpand }) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />}
type="device"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={deviceFilter}
renderLabel={({ x }) => getDeviceMessage(x)}

View file

@ -3,7 +3,7 @@ import tinycolor from 'tinycolor2';
import BarChart from './BarChart';
import { getTimezone, getDateArray, getDateLength } from 'lib/date';
import useFetch from 'hooks/useFetch';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
const COLORS = [
'#2680eb',
@ -16,7 +16,7 @@ const COLORS = [
'#85d044',
];
export default function EventsChart({ websiteId }) {
export default function EventsChart({ websiteId, token }) {
const dateRange = useDateRange(websiteId);
const { startDate, endDate, unit, modified } = dateRange;
const { data } = useFetch(
@ -26,6 +26,7 @@ export default function EventsChart({ websiteId }) {
end_at: +endDate,
unit,
tz: getTimezone(),
token,
},
{ update: [modified] },
);

View file

@ -3,13 +3,14 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css';
export default function EventsTable({ websiteId, limit, onExpand, onDataLoad }) {
export default function EventsTable({ websiteId, token, limit, onExpand, onDataLoad }) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
type="event"
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
websiteId={websiteId}
token={token}
limit={limit}
renderLabel={({ x }) => <Label value={x} />}
onExpand={onExpand}

View file

@ -3,12 +3,12 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Loading from 'components/common/Loading';
import useFetch from 'hooks/useFetch';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard';
import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId, className }) {
export default function MetricsBar({ websiteId, token, className }) {
const dateRange = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
const { data } = useFetch(
@ -16,6 +16,7 @@ export default function MetricsBar({ websiteId, className }) {
{
start_at: +startDate,
end_at: +endDate,
token,
},
{
update: [modified],

View file

@ -10,12 +10,13 @@ import useFetch from 'hooks/useFetch';
import Arrow from 'assets/arrow-right.svg';
import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
import styles from './MetricsTable.module.css';
export default function MetricsTable({
websiteId,
websiteDomain,
token,
title,
metric,
type,
@ -37,6 +38,7 @@ export default function MetricsTable({
start_at: +startDate,
end_at: +endDate,
domain: websiteDomain,
token,
},
{ onDataLoad, delay: 300, update: [modified] },
);

View file

@ -3,13 +3,14 @@ import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function OSTable({ websiteId, limit, onExpand }) {
export default function OSTable({ websiteId, token, limit, onExpand }) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />}
type="os"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={osFilter}
onExpand={onExpand}

View file

@ -5,7 +5,7 @@ import { urlFilter } from 'lib/filters';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import MetricsTable from './MetricsTable';
export default function PagesTable({ websiteId, websiteDomain, limit, onExpand }) {
export default function PagesTable({ websiteId, token, websiteDomain, limit, onExpand }) {
const [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [
@ -25,6 +25,7 @@ export default function PagesTable({ websiteId, websiteDomain, limit, onExpand }
limit ? null : <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}

View file

@ -1,30 +0,0 @@
import React from 'react';
import ButtonGroup from 'components/common/ButtonGroup';
import { getDateRange } from 'lib/date';
import styles from './QuickButtons.module.css';
const options = [
{ label: '24h', value: '24hour' },
{ label: '7d', value: '7day' },
{ label: '30d', value: '30day' },
];
export default function QuickButtons({ value, onChange }) {
const selectedItem = options.find(item => item.value === value)?.value;
function handleClick(selected) {
if (selected !== value) {
onChange(getDateRange(selected));
}
}
return (
<ButtonGroup
size="xsmall"
className={styles.buttons}
items={options}
selectedItem={selectedItem}
onClick={handleClick}
/>
);
}

View file

@ -1,33 +0,0 @@
.buttons {
display: flex;
align-content: center;
position: absolute;
top: 0;
right: 0;
margin: auto;
}
.buttons button + button {
margin-left: 10px;
}
.buttons .button {
font-size: var(--font-size-xsmall);
padding: 4px 8px;
}
.active {
font-weight: 600;
}
@media only screen and (max-width: 768px) {
.buttons button:last-child {
display: none;
}
}
@media only screen and (max-width: 576px) {
.buttons {
display: none;
}
}

View file

@ -5,7 +5,13 @@ import { refFilter } from 'lib/filters';
import ButtonGroup from 'components/common/ButtonGroup';
import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpand = () => {} }) {
export default function ReferrersTable({
websiteId,
websiteDomain,
token,
limit,
onExpand = () => {},
}) {
const [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [
@ -20,9 +26,9 @@ export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpa
{ label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
];
const renderLink = ({ x: url }) => {
return url.startsWith('http') ? (
<a href={url} target="_blank" rel="noreferrer">
const renderLink = ({ w: href, x: url }) => {
return (href || url).startsWith('http') ? (
<a href={href || url} target="_blank" rel="noreferrer">
{decodeURI(url)}
</a>
) : (
@ -40,6 +46,7 @@ export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpa
}
websiteId={websiteId}
websiteDomain={websiteDomain}
token={token}
limit={limit}
dataFilter={refFilter}
filterOptions={{

View file

@ -3,17 +3,18 @@ import { useDispatch } from 'react-redux';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/common/DateFilter';
import StickyHeader from 'components/helpers/StickyHeader';
import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange';
import { getDateArray, getDateLength, getTimezone } from 'lib/date';
import { setDateRange } from 'redux/actions/websites';
import styles from './WebsiteChart.module.css';
import WebsiteHeader from './WebsiteHeader';
import { useDateRange } from '../../hooks/useDateRange';
export default function WebsiteChart({
websiteId,
token,
title,
stickyHeader = false,
showLink = false,
@ -30,6 +31,7 @@ export default function WebsiteChart({
end_at: +endDate,
unit,
tz: getTimezone(),
token,
},
{ onDataLoad, update: [modified] },
);
@ -50,7 +52,7 @@ export default function WebsiteChart({
return (
<>
<WebsiteHeader websiteId={websiteId} title={title} showLink={showLink} />
<WebsiteHeader websiteId={websiteId} token={token} title={title} showLink={showLink} />
<div className={classNames(styles.header, 'row')}>
<StickyHeader
className={classNames(styles.metrics, 'col row')}
@ -58,7 +60,7 @@ export default function WebsiteChart({
enabled={stickyHeader}
>
<div className="col-12 col-lg-9">
<MetricsBar websiteId={websiteId} />
<MetricsBar websiteId={websiteId} token={token} />
</div>
<div className={classNames(styles.filter, 'col-12 col-lg-3')}>
<DateFilter

View file

@ -2,18 +2,18 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link';
import PageHeader from 'components/layout/PageHeader';
import RefreshButton from 'components/common/RefreshButton';
import ButtonLayout from 'components/layout/ButtonLayout';
import Icon from 'components/common/Icon';
import ActiveUsers from './ActiveUsers';
import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteHeader.module.css';
import RefreshButton from '../common/RefreshButton';
import ButtonLayout from '../layout/ButtonLayout';
import Icon from '../common/Icon';
export default function WebsiteHeader({ websiteId, title, showLink = false }) {
export default function WebsiteHeader({ websiteId, token, title, showLink = false }) {
return (
<PageHeader>
<div className={styles.title}>{title}</div>
<ActiveUsers className={styles.active} websiteId={websiteId} />
<ActiveUsers className={styles.active} websiteId={websiteId} token={token} />
<ButtonLayout>
<RefreshButton websiteId={websiteId} />
{showLink && (

View file

@ -92,7 +92,7 @@ export default function AccountSettings() {
<>
<PageHeader>
<div>
<FormattedMessage id="settings.accounts" defaultMessage="Accounts" />
<FormattedMessage id="label.accounts" defaultMessage="Accounts" />
</div>
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}>
<div>

View file

@ -1,29 +1,44 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
import Modal from 'components/common/Modal';
import Toast from 'components/common/Toast';
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
import DateFilter from 'components/common/DateFilter';
import Dots from 'assets/ellipsis-h.svg';
import { getTimezone } from 'lib/date';
import { setItem } from 'lib/web';
import useDateRange from 'hooks/useDateRange';
import { setDateRange } from 'redux/actions/websites';
import styles from './ProfileSettings.module.css';
export default function ProfileSettings() {
const dispatch = useDispatch();
const user = useSelector(state => state.user);
const [changePassword, setChangePassword] = useState(false);
const [message, setMessage] = useState();
const { user_id } = user;
const timezone = getTimezone();
const dateRange = useDateRange(0);
const { startDate, endDate, value } = dateRange;
function handleSave() {
setChangePassword(false);
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
}
function handleDateChange(values) {
setItem(`umami.date-range`, values);
dispatch(setDateRange(0, values));
}
return (
<>
<PageHeader>
<div>
<FormattedMessage id="settings.profile" defaultMessage="Profile" />
<FormattedMessage id="label.profile" defaultMessage="Profile" />
</div>
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}>
<div>
@ -36,6 +51,21 @@ export default function ProfileSettings() {
<FormattedMessage id="label.username" defaultMessage="Username" />
</dt>
<dd>{user.username}</dd>
<dt>
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
</dt>
<dd>{timezone}</dd>
<dt>
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" />
</dt>
<dd className={styles.date}>
<DateFilter
value={value}
startDate={startDate}
endDate={endDate}
onChange={handleDateChange}
/>
</dd>
</dl>
{changePassword && (
<Modal

View file

@ -0,0 +1,3 @@
.date {
display: flex;
}

View file

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import Page from 'components/layout/Page';
import MenuLayout from 'components/layout/MenuLayout';
import WebsiteSettings from './WebsiteSettings';
@ -7,33 +8,38 @@ import ProfileSettings from './ProfileSettings';
import { useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
const WEBSITES = 1;
const ACCOUNTS = 2;
const PROFILE = 3;
const WEBSITES = '/settings';
const ACCOUNTS = '/settings/accounts';
const PROFILE = '/settings/profile';
export default function Settings() {
const user = useSelector(state => state.user);
const [option, setOption] = useState(WEBSITES);
const router = useRouter();
const { pathname } = router;
const menuOptions = [
{
label: <FormattedMessage id="settings.websites" defaultMessage="Websites" />,
label: <FormattedMessage id="label.websites" defaultMessage="Websites" />,
value: WEBSITES,
},
{
label: <FormattedMessage id="settings.accounts" defaultMessage="Accounts" />,
label: <FormattedMessage id="label.accounts" defaultMessage="Accounts" />,
value: ACCOUNTS,
hidden: !user.is_admin,
},
{ label: <FormattedMessage id="settings.profile" defaultMessage="Profile" />, value: PROFILE },
{
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
value: PROFILE,
},
];
return (
<Page>
<MenuLayout menu={menuOptions} selectedOption={option} onMenuSelect={setOption}>
{option === WEBSITES && <WebsiteSettings />}
{option === ACCOUNTS && <AccountSettings />}
{option === PROFILE && <ProfileSettings />}
{pathname === WEBSITES && <WebsiteSettings />}
{pathname === ACCOUNTS && <AccountSettings />}
{pathname === PROFILE && <ProfileSettings />}
</MenuLayout>
</Page>
);

View file

@ -37,7 +37,7 @@ export default function WebsiteSettings() {
<Button
icon={<LinkIcon />}
size="small"
tooltip={<FormattedMessage id="tooltip.get-share-url" defaultMessage="Get share URL" />}
tooltip={<FormattedMessage id="message.get-share-url" defaultMessage="Get share URL" />}
tooltipId={`button-share-${row.website_id}`}
onClick={() => setShowUrl(row)}
/>
@ -46,7 +46,7 @@ export default function WebsiteSettings() {
icon={<Code />}
size="small"
tooltip={
<FormattedMessage id="tooltip.get-tracking-code" defaultMessage="Get tracking code" />
<FormattedMessage id="message.get-tracking-code" defaultMessage="Get tracking code" />
}
tooltipId={`button-code-${row.website_id}`}
onClick={() => setShowCode(row)}
@ -111,7 +111,7 @@ export default function WebsiteSettings() {
<EmptyPlaceholder
msg={
<FormattedMessage
id="placeholder.message.no-websites-configured"
id="message.no-websites-configured"
defaultMessage="You don't have any websites configured."
/>
}
@ -128,7 +128,7 @@ export default function WebsiteSettings() {
<>
<PageHeader>
<div>
<FormattedMessage id="settings.websites" defaultMessage="Websites" />
<FormattedMessage id="label.websites" defaultMessage="Websites" />
</div>
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}>
<div>