mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Updates to realtime. Fixed refresh button.
This commit is contained in:
parent
638a674e99
commit
28921a7cd5
@ -1,68 +1,74 @@
|
||||
import { endOfYear, isSameDay } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
import { Icon, Modal, Dropdown, Item } from 'react-basics';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { endOfYear, isSameDay } from 'date-fns';
|
||||
import DatePickerForm from 'components/metrics/DatePickerForm';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import Calendar from 'assets/calendar.svg';
|
||||
import { dateFormat, getDateRangeValues } from 'lib/date';
|
||||
import Icons from 'components/icons';
|
||||
import { labels } from 'components/messages';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
|
||||
const messages = defineMessages({
|
||||
today: { id: 'label.today', defaultMessage: 'Today' },
|
||||
lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' },
|
||||
yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' },
|
||||
thisWeek: { id: 'label.this-week', defaultMessage: 'This week' },
|
||||
lastDays: { id: 'label.last-days', defaultMessage: 'Last {x} days' },
|
||||
thisMonth: { id: 'label.this-month', defaultMessage: 'This month' },
|
||||
thisYear: { id: 'label.this-year', defaultMessage: 'This year' },
|
||||
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
|
||||
customRange: { id: 'label.custom-range', defaultMessage: 'Custom-range' },
|
||||
});
|
||||
|
||||
function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
function DateFilter({ websiteId, value, className }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { get } = useApi();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
|
||||
async function handleDateChange(value) {
|
||||
if (value === 'all') {
|
||||
const data = await get(`/websites/${websiteId}`);
|
||||
|
||||
if (data) {
|
||||
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
|
||||
}
|
||||
} else {
|
||||
setDateRange(value);
|
||||
}
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ label: formatMessage(messages.today), value: '1day' },
|
||||
{ label: formatMessage(labels.today), value: '1day' },
|
||||
{
|
||||
label: formatMessage(messages.lastHours, { x: 24 }),
|
||||
label: formatMessage(labels.lastHours, { x: 24 }),
|
||||
value: '24hour',
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.yesterday),
|
||||
label: formatMessage(labels.yesterday),
|
||||
value: '-1day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.thisWeek),
|
||||
label: formatMessage(labels.thisWeek),
|
||||
value: '1week',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.lastDays, { x: 7 }),
|
||||
label: formatMessage(labels.lastDays, { x: 7 }),
|
||||
value: '7day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.thisMonth),
|
||||
label: formatMessage(labels.thisMonth),
|
||||
value: '1month',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.lastDays, { x: 30 }),
|
||||
label: formatMessage(labels.lastDays, { x: 30 }),
|
||||
value: '30day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.lastDays, { x: 90 }),
|
||||
label: formatMessage(labels.lastDays, { x: 90 }),
|
||||
value: '90day',
|
||||
},
|
||||
{ label: formatMessage(messages.thisYear), value: '1year' },
|
||||
{ label: formatMessage(labels.thisYear), value: '1year' },
|
||||
{
|
||||
label: formatMessage(messages.allTime),
|
||||
label: formatMessage(labels.allTime),
|
||||
value: 'all',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.customRange),
|
||||
label: formatMessage(labels.customRange),
|
||||
value: 'custom',
|
||||
divider: true,
|
||||
},
|
||||
@ -76,17 +82,17 @@ function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleChange = async value => {
|
||||
const handleChange = value => {
|
||||
if (value === 'custom') {
|
||||
setShowPicker(true);
|
||||
return;
|
||||
}
|
||||
onChange(value);
|
||||
handleDateChange(value);
|
||||
};
|
||||
|
||||
const handlePickerChange = value => {
|
||||
setShowPicker(false);
|
||||
onChange(value);
|
||||
handleDateChange(value);
|
||||
};
|
||||
|
||||
const handleClose = () => setShowPicker(false);
|
||||
@ -98,9 +104,14 @@ function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
items={options}
|
||||
renderValue={renderValue}
|
||||
value={value}
|
||||
alignment="end"
|
||||
onChange={handleChange}
|
||||
>
|
||||
{({ label, value }) => <Item key={value}>{label}</Item>}
|
||||
{({ label, value, divider }) => (
|
||||
<Item key={value} divider={divider}>
|
||||
{label}
|
||||
</Item>
|
||||
)}
|
||||
</Dropdown>
|
||||
{showPicker && (
|
||||
<Modal onClose={handleClose}>
|
||||
@ -128,13 +139,15 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flexbox gap={10} alignItems="center" wrap="nowrap">
|
||||
<Icon className="mr-2" onClick={handleClick}>
|
||||
<Calendar />
|
||||
<Icons.Calendar />
|
||||
</Icon>
|
||||
{dateFormat(startDate, 'd LLL y', locale)}
|
||||
{!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`}
|
||||
</>
|
||||
<Text>
|
||||
{dateFormat(startDate, 'd LLL y', locale)}
|
||||
{!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -6,13 +6,13 @@ import { Icon, Icons } from 'react-basics';
|
||||
import styles from './FilterLink.module.css';
|
||||
|
||||
export default function FilterLink({ id, value, label, externalUrl }) {
|
||||
const { resolve, query } = usePageQuery();
|
||||
const { resolveUrl, query } = usePageQuery();
|
||||
const active = query[id] !== undefined;
|
||||
const selected = query[id] === value;
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<Link href={resolve({ [id]: value })} replace>
|
||||
<Link href={resolveUrl({ [id]: value })} replace>
|
||||
<a
|
||||
className={classNames(styles.label, {
|
||||
[styles.inactive]: active && !selected,
|
||||
|
@ -4,6 +4,7 @@ import Bolt from 'assets/bolt.svg';
|
||||
import Calendar from 'assets/calendar.svg';
|
||||
import Clock from 'assets/clock.svg';
|
||||
import Dashboard from 'assets/dashboard.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import Gear from 'assets/gear.svg';
|
||||
import Globe from 'assets/globe.svg';
|
||||
import Lock from 'assets/lock.svg';
|
||||
@ -13,6 +14,7 @@ import Profile from 'assets/profile.svg';
|
||||
import Sun from 'assets/sun.svg';
|
||||
import User from 'assets/user.svg';
|
||||
import Users from 'assets/users.svg';
|
||||
import Visitor from 'assets/visitor.svg';
|
||||
|
||||
const icons = {
|
||||
...Icons,
|
||||
@ -21,6 +23,7 @@ const icons = {
|
||||
Calendar,
|
||||
Clock,
|
||||
Dashboard,
|
||||
Eye,
|
||||
Gear,
|
||||
Globe,
|
||||
Lock,
|
||||
@ -30,6 +33,7 @@ const icons = {
|
||||
Sun,
|
||||
User,
|
||||
Users,
|
||||
Visitor,
|
||||
};
|
||||
|
||||
export default icons;
|
||||
|
@ -1,22 +1,16 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Button, Icon, Tooltip } from 'react-basics';
|
||||
import useStore from 'store/queries';
|
||||
import { LoadingButton, Icon, Tooltip } from 'react-basics';
|
||||
import { setDateRange } from 'store/websites';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import Icons from 'components/icons';
|
||||
import { labels } from 'components/messages';
|
||||
|
||||
function RefreshButton({ websiteId }) {
|
||||
function RefreshButton({ websiteId, isLoading }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const selector = useCallback(state => state[`/websites/${websiteId}/stats`], [websiteId]);
|
||||
const completed = useStore(selector);
|
||||
|
||||
function handleClick() {
|
||||
if (!loading && dateRange) {
|
||||
setLoading(true);
|
||||
if (!isLoading && dateRange) {
|
||||
if (/^\d+/.test(dateRange.value)) {
|
||||
setDateRange(websiteId, dateRange.value);
|
||||
} else {
|
||||
@ -25,17 +19,13 @@ function RefreshButton({ websiteId }) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [completed]);
|
||||
|
||||
return (
|
||||
<Tooltip label={formatMessage(labels.refresh)}>
|
||||
<Button onClick={handleClick}>
|
||||
<LoadingButton loading={isLoading} onClick={handleClick}>
|
||||
<Icon>
|
||||
<Icons.Refresh />
|
||||
</Icon>
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
13
components/layout/Grid.js
Normal file
13
components/layout/Grid.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { Row, Column } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Grid.module.css';
|
||||
|
||||
export function GridRow(props) {
|
||||
const { className, ...otherProps } = props;
|
||||
return <Row {...otherProps} className={classNames(styles.row, className)} />;
|
||||
}
|
||||
|
||||
export function GridColumn(props) {
|
||||
const { className, ...otherProps } = props;
|
||||
return <Column {...otherProps} className={classNames(styles.col, className)} />;
|
||||
}
|
35
components/layout/Grid.module.css
Normal file
35
components/layout/Grid.module.css
Normal file
@ -0,0 +1,35 @@
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid var(--base300);
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-left: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.row > .col:first-child {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.row > .col:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.row {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-top: 1px solid var(--base300);
|
||||
border-left: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
@ -81,9 +81,21 @@ export const labels = defineMessages({
|
||||
visitors: { id: 'label.visitors', defaultMessage: 'Visitors' },
|
||||
filterCombined: { id: 'label.filter-combined', defaultMessage: 'Combined' },
|
||||
filterRaw: { id: 'label.filter-raw', defaultMessage: 'Raw' },
|
||||
views: { id: 'label.views', defaultMessage: 'View' },
|
||||
views: { id: 'label.views', defaultMessage: 'Views' },
|
||||
none: { id: 'label.none', defaultMessage: 'None' },
|
||||
clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' },
|
||||
today: { id: 'label.today', defaultMessage: 'Today' },
|
||||
lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' },
|
||||
yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' },
|
||||
thisWeek: { id: 'label.this-week', defaultMessage: 'This week' },
|
||||
lastDays: { id: 'label.last-days', defaultMessage: 'Last {x} days' },
|
||||
thisMonth: { id: 'label.this-month', defaultMessage: 'This month' },
|
||||
thisYear: { id: 'label.this-year', defaultMessage: 'This year' },
|
||||
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
|
||||
customRange: { id: 'label.custom-range', defaultMessage: 'Custom-range' },
|
||||
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
|
||||
all: { id: 'label.all', defaultMessage: 'All' },
|
||||
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
@ -13,6 +13,7 @@ export default function ActiveUsers({ websiteId, value, refetchInterval = 60000
|
||||
() => get(`/websites/${websiteId}/active`),
|
||||
{
|
||||
refetchInterval,
|
||||
enabled: !!websiteId,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -42,7 +42,7 @@ export default function DatePickerForm({
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filter}>
|
||||
<ButtonGroup size="sm" selectedKey={selected} onSelect={setSelected}>
|
||||
<ButtonGroup selectedKey={selected} onSelect={setSelected}>
|
||||
<Button key={FILTER_DAY}>{formatMessage(labels.singleDay)}</Button>
|
||||
<Button key={FILTER_RANGE}>{formatMessage(labels.dateRange)}</Button>
|
||||
</ButtonGroup>
|
||||
|
@ -1,26 +1,39 @@
|
||||
import { useIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { safeDecodeURI } from 'next-basics';
|
||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
import { labels } from 'components/messages';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import styles from './FilterTags.module.css';
|
||||
|
||||
export default function FilterTags({ className, params, onClick }) {
|
||||
export default function FilterTags({ websiteId, params, onClick }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
router,
|
||||
resolveUrl,
|
||||
query: { view },
|
||||
} = usePageQuery();
|
||||
|
||||
if (Object.keys(params).filter(key => params[key]).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleCloseFilter(param) {
|
||||
if (param === null) {
|
||||
router.push(`/websites/${websiteId}/?view=${view}`);
|
||||
} else {
|
||||
router.push(resolveUrl({ [param]: undefined }));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.filters, className)}>
|
||||
<div className={styles.filters}>
|
||||
{Object.keys(params).map(key => {
|
||||
if (!params[key]) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key={key} className={styles.tag}>
|
||||
<Button onClick={() => onClick(key)} variant="primary" size="sm">
|
||||
<Button onClick={() => handleCloseFilter(key)} variant="primary" size="sm">
|
||||
<Text>
|
||||
<b>{`${key}`}</b> — {`${safeDecodeURI(params[key])}`}
|
||||
</Text>
|
||||
@ -31,7 +44,7 @@ export default function FilterTags({ className, params, onClick }) {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button size="sm" variant="quiet" onClick={() => onClick(null)}>
|
||||
<Button size="sm" variant="quiet" onClick={() => handleCloseFilter(null)}>
|
||||
<Icon>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
|
@ -31,7 +31,7 @@ export default function MetricsTable({
|
||||
}) {
|
||||
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
|
||||
const {
|
||||
resolve,
|
||||
resolveUrl,
|
||||
router,
|
||||
query: { url, referrer, os, browser, device, country },
|
||||
} = usePageQuery();
|
||||
@ -79,7 +79,7 @@ export default function MetricsTable({
|
||||
{data && !error && <DataTable {...props} data={filteredData} className={className} />}
|
||||
<div className={styles.footer}>
|
||||
{data && !error && limit && (
|
||||
<Link href={router.pathname} as={resolve({ view: type })}>
|
||||
<Link href={router.pathname} as={resolveUrl({ view: type })}>
|
||||
<a>
|
||||
<Button variant="quiet">
|
||||
<Text>{formatMessage(messages.more)}</Text>
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { differenceInMinutes } from 'date-fns';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import ActiveUsers from './ActiveUsers';
|
||||
import MetricCard from './MetricCard';
|
||||
import styles from './RealtimeHeader.module.css';
|
||||
|
||||
export default function RealtimeHeader({ data, websiteId }) {
|
||||
const { pageviews, sessions, events, countries } = data;
|
||||
|
||||
const count = useMemo(() => {
|
||||
return sessions.filter(
|
||||
({ createdAt }) => differenceInMinutes(new Date(), new Date(createdAt)) <= 5,
|
||||
).length;
|
||||
}, [sessions, websiteId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader>
|
||||
<div>
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</div>
|
||||
<div>
|
||||
<ActiveUsers className={styles.active} value={count} />
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className={styles.metrics}>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
value={pageviews.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
value={sessions.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
value={events.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
||||
value={countries.length}
|
||||
hideComparison
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
.metrics {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.active {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Button, Icon, Text, Row, Column, Container } from 'react-basics';
|
||||
import { Button, Icon, Text, Row, Column, Flexbox } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import PageviewsChart from './PageviewsChart';
|
||||
import MetricsBar from './MetricsBar';
|
||||
@ -9,6 +9,7 @@ import DateFilter from 'components/common/DateFilter';
|
||||
import StickyHeader from 'components/helpers/StickyHeader';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import RefreshButton from 'components/input/RefreshButton';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
@ -28,13 +29,11 @@ export default function WebsiteChart({
|
||||
onDataLoad = () => {},
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, value, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
const {
|
||||
router,
|
||||
resolve,
|
||||
query: { view, url, referrer, os, browser, device, country },
|
||||
query: { url, referrer, os, browser, device, country },
|
||||
} = usePageQuery();
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
@ -66,26 +65,6 @@ export default function WebsiteChart({
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
function handleCloseFilter(param) {
|
||||
if (param === null) {
|
||||
router.push(`/websites/${websiteId}/?view=${view}`);
|
||||
} else {
|
||||
router.push(resolve({ [param]: undefined }));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDateChange(value) {
|
||||
if (value === 'all') {
|
||||
const data = await get(`/websites/${websiteId}`);
|
||||
|
||||
if (data) {
|
||||
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
|
||||
}
|
||||
} else {
|
||||
setDateRange(value);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} title={title} domain={domain}>
|
||||
@ -102,22 +81,15 @@ export default function WebsiteChart({
|
||||
</Link>
|
||||
)}
|
||||
</WebsiteHeader>
|
||||
<FilterTags
|
||||
params={{ url, referrer, os, browser, device, country }}
|
||||
onClick={handleCloseFilter}
|
||||
/>
|
||||
<FilterTags websiteId={websiteId} params={{ url, referrer, os, browser, device, country }} />
|
||||
<StickyHeader stickyClassName={styles.sticky} enabled={stickyHeader}>
|
||||
<Row className={styles.header}>
|
||||
<Column xs={12} sm={12} md={12} defaultSize={10}>
|
||||
<Column>
|
||||
<MetricsBar websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column className={styles.filter} xs={12} sm={12} md={12} defaultSize={2}>
|
||||
<DateFilter
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
<Column className={styles.actions}>
|
||||
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
|
||||
<DateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
|
||||
</Column>
|
||||
</Row>
|
||||
</StickyHeader>
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
.chart {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -32,9 +32,17 @@
|
||||
border-bottom: 1px solid var(--base300);
|
||||
z-index: 3;
|
||||
width: inherit;
|
||||
padding-top: 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.filter {
|
||||
align-self: center;
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
@ -1,21 +1,25 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { Row, Column } from 'react-basics';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { subMinutes, startOfMinute } from 'date-fns';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { subMinutes, startOfMinute, differenceInMinutes } from 'date-fns';
|
||||
import firstBy from 'thenby';
|
||||
import { GridRow, GridColumn } from 'components/layout/Grid';
|
||||
import Page from 'components/layout/Page';
|
||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||
import RealtimeLog from 'components/metrics/RealtimeLog';
|
||||
import RealtimeHeader from 'components/metrics/RealtimeHeader';
|
||||
import RealtimeLog from 'components/pages/realtime/RealtimeLog';
|
||||
import RealtimeHeader from 'components/pages/realtime/RealtimeHeader';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
import DataTable from 'components/metrics/DataTable';
|
||||
import RealtimeViews from 'components/metrics/RealtimeViews';
|
||||
import RealtimeViews from 'components/pages/realtime/RealtimeViews';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { labels } from 'components/messages';
|
||||
import { SHARE_TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
||||
import styles from './RealtimeDashboard.module.css';
|
||||
import StickyHeader from 'components/helpers/StickyHeader';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import ActiveUsers from 'components/metrics/ActiveUsers';
|
||||
|
||||
function mergeData(state, data, time) {
|
||||
const ids = state.map(({ __id }) => __id);
|
||||
@ -29,18 +33,19 @@ function filterWebsite(data, id) {
|
||||
}
|
||||
|
||||
export default function RealtimeDashboard() {
|
||||
const { formatMessage } = useIntl();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const [data, setData] = useState();
|
||||
const [websiteId, setWebsiteId] = useState(null);
|
||||
const [websiteId, setWebsiteId] = useState();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: init, isLoading } = useQuery(['realtime:init'], () => get('/realtime/init'));
|
||||
const { data: websites, isLoading } = useQuery(['websites:me'], () => get('/me/websites'));
|
||||
|
||||
const { data: updates } = useQuery(
|
||||
['realtime:updates'],
|
||||
() =>
|
||||
get('/realtime/update', { startAt: data?.timestamp }, { [SHARE_TOKEN_HEADER]: init?.token }),
|
||||
() => get('/realtime/update', { startAt: data?.timestamp }),
|
||||
{
|
||||
disabled: !init?.websites?.length || !data,
|
||||
enabled: !!websiteId,
|
||||
retryInterval: REALTIME_INTERVAL,
|
||||
},
|
||||
);
|
||||
@ -55,7 +60,7 @@ export default function RealtimeDashboard() {
|
||||
const { pageviews, sessions, events } = data;
|
||||
|
||||
if (websiteId) {
|
||||
const { id } = init.websites.find(n => n.id === websiteId);
|
||||
const { id } = websites.find(n => n.id === websiteId);
|
||||
return {
|
||||
pageviews: filterWebsite(pageviews, id),
|
||||
sessions: filterWebsite(sessions, id),
|
||||
@ -67,6 +72,15 @@ export default function RealtimeDashboard() {
|
||||
return data;
|
||||
}, [data, websiteId]);
|
||||
|
||||
const count = useMemo(() => {
|
||||
if (data) {
|
||||
const { sessions } = data;
|
||||
return sessions.filter(
|
||||
({ createdAt }) => differenceInMinutes(new Date(), new Date(createdAt)) <= 5,
|
||||
).length;
|
||||
}
|
||||
}, [data, websiteId]);
|
||||
|
||||
const countries = useMemo(() => {
|
||||
if (realtimeData?.sessions) {
|
||||
return percentFilter(
|
||||
@ -89,14 +103,6 @@ export default function RealtimeDashboard() {
|
||||
return [];
|
||||
}, [realtimeData?.sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (init && !data) {
|
||||
const { websites, data } = init;
|
||||
|
||||
setData({ websites, ...data });
|
||||
}
|
||||
}, [init]);
|
||||
|
||||
useEffect(() => {
|
||||
if (updates) {
|
||||
const { pageviews, sessions, events, timestamp } = updates;
|
||||
@ -112,44 +118,43 @@ export default function RealtimeDashboard() {
|
||||
}
|
||||
}, [updates]);
|
||||
|
||||
if (!init || !data || isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { websites } = data;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<RealtimeHeader
|
||||
websites={websites}
|
||||
websiteId={websiteId}
|
||||
data={{ ...realtimeData, countries }}
|
||||
onSelect={setWebsiteId}
|
||||
/>
|
||||
<Page loading={isLoading || !websites}>
|
||||
<PageHeader title={formatMessage(labels.realtime)}>
|
||||
<ActiveUsers value={count} />
|
||||
</PageHeader>
|
||||
<StickyHeader stickyClassName={styles.sticky}>
|
||||
<RealtimeHeader
|
||||
websites={websites}
|
||||
websiteId={websiteId}
|
||||
data={{ ...realtimeData, countries }}
|
||||
onSelect={setWebsiteId}
|
||||
/>
|
||||
</StickyHeader>
|
||||
<div className={styles.chart}>
|
||||
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
|
||||
</div>
|
||||
<Row>
|
||||
<Column xs={12} lg={4}>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} lg={4} xl={4}>
|
||||
<RealtimeViews websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</Column>
|
||||
<Column xs={12} lg={8}>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} sm={12} md={12} lg={8} xl={8}>
|
||||
<RealtimeLog websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column xs={12} lg={4}>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} lg={4}>
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
title={formatMessage(labels.countries)}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
data={countries}
|
||||
renderLabel={renderCountryName}
|
||||
/>
|
||||
</Column>
|
||||
<Column xs={12} lg={8}>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} lg={8}>
|
||||
<WorldMap data={countries} />
|
||||
</Column>
|
||||
</Row>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
@ -5,3 +5,12 @@
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: var(--base50);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
z-index: 3;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
44
components/pages/realtime/RealtimeHeader.js
Normal file
44
components/pages/realtime/RealtimeHeader.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Dropdown, Item } from 'react-basics';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import { labels } from 'components/messages';
|
||||
import styles from './RealtimeHeader.module.css';
|
||||
|
||||
export default function RealtimeHeader({ data, websiteId, websites, onSelect }) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { pageviews, sessions, events, countries } = data;
|
||||
|
||||
const renderValue = value => {
|
||||
return websites?.find(({ id }) => id === value)?.name;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.metrics}>
|
||||
<MetricCard label={formatMessage(labels.views)} value={pageviews?.length} hideComparison />
|
||||
<MetricCard
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={sessions?.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard label={formatMessage(labels.events)} value={events?.length} hideComparison />
|
||||
<MetricCard
|
||||
label={formatMessage(labels.countries)}
|
||||
value={countries.length}
|
||||
hideComparison
|
||||
/>
|
||||
</div>
|
||||
<Dropdown
|
||||
items={websites}
|
||||
value={websiteId}
|
||||
renderValue={renderValue}
|
||||
onChange={onSelect}
|
||||
alignment="end"
|
||||
placeholder={formatMessage(labels.selectWebsite)}
|
||||
>
|
||||
{item => <Item key={item.id}>{item.name}</Item>}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
9
components/pages/realtime/RealtimeHeader.module.css
Normal file
9
components/pages/realtime/RealtimeHeader.module.css
Normal file
@ -0,0 +1,9 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
}
|
@ -1,41 +1,42 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { StatusLight } from 'react-basics';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { StatusLight, Icon } from 'react-basics';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import firstBy from 'thenby';
|
||||
import { Icon } from 'react-basics';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import NoData from 'components/common/NoData';
|
||||
import { getDeviceMessage, labels } from 'components/messages';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
import Bolt from 'assets/bolt.svg';
|
||||
import Visitor from 'assets/visitor.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import { safeDecodeURI } from 'next-basics';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
|
||||
const TYPE_ALL = 0;
|
||||
const TYPE_PAGEVIEW = 1;
|
||||
const TYPE_SESSION = 2;
|
||||
const TYPE_EVENT = 3;
|
||||
const TYPE_ALL = 'type-all';
|
||||
const TYPE_PAGEVIEW = 'type-pageview';
|
||||
const TYPE_SESSION = 'type-session';
|
||||
const TYPE_EVENT = 'type-event';
|
||||
|
||||
const TYPE_ICONS = {
|
||||
[TYPE_PAGEVIEW]: <Eye />,
|
||||
[TYPE_SESSION]: <Visitor />,
|
||||
[TYPE_EVENT]: <Bolt />,
|
||||
[TYPE_PAGEVIEW]: <Icons.Eye />,
|
||||
[TYPE_SESSION]: <Icons.Visitor />,
|
||||
[TYPE_EVENT]: <Icons.Bolt />,
|
||||
};
|
||||
|
||||
export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
const intl = useIntl();
|
||||
const { formatMessage } = useIntl();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const [filter, setFilter] = useState(TYPE_ALL);
|
||||
|
||||
const logs = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { pageviews, sessions, events } = data;
|
||||
const logs = [...pageviews, ...sessions, ...events].sort(firstBy('createdAt', -1));
|
||||
if (filter) {
|
||||
@ -45,6 +46,10 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
}, [data, filter]);
|
||||
|
||||
const uuids = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.sessions.reduce((obj, { sessionId, sessionUuid }) => {
|
||||
obj[sessionId] = sessionUuid;
|
||||
return obj;
|
||||
@ -53,19 +58,19 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: <FormattedMessage id="label.all" defaultMessage="All" />,
|
||||
label: formatMessage(labels.all),
|
||||
key: TYPE_ALL,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.views" defaultMessage="Views" />,
|
||||
label: formatMessage(labels.views),
|
||||
key: TYPE_PAGEVIEW,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />,
|
||||
label: formatMessage(labels.sessions),
|
||||
key: TYPE_SESSION,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
|
||||
label: formatMessage(labels.events),
|
||||
key: TYPE_EVENT,
|
||||
},
|
||||
];
|
||||
@ -124,10 +129,10 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
id="message.log.visitor"
|
||||
defaultMessage="Visitor from {country} using {browser} on {os} {device}"
|
||||
values={{
|
||||
country: <b>{countryNames[country] || intl.formatMessage(labels.unknown)}</b>,
|
||||
country: <b>{countryNames[country] || formatMessage(labels.unknown)}</b>,
|
||||
browser: <b>{BROWSERS[browser]}</b>,
|
||||
os: <b>{os}</b>,
|
||||
device: <b>{intl.formatMessage(getDeviceMessage(device))}</b>,
|
||||
device: <b>{formatMessage(getDeviceMessage(device))}</b>,
|
||||
}}
|
||||
/>
|
||||
);
|
@ -1,12 +1,14 @@
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { ButtonGroup, Button } from 'react-basics';
|
||||
import { useIntl } from 'react-intl';
|
||||
import firstBy from 'thenby';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import DataTable from './DataTable';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import DataTable from 'components/metrics/DataTable';
|
||||
import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants';
|
||||
import { labels } from 'components/messages';
|
||||
|
||||
export default function RealtimeViews({ websiteId, data, websites }) {
|
||||
export default function RealtimeViews({ websiteId, data = {}, websites }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { pageviews } = data;
|
||||
const [filter, setFilter] = useState(FILTER_REFERRERS);
|
||||
const domains = useMemo(() => websites.map(({ domain }) => domain), [websites]);
|
||||
@ -20,11 +22,11 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />,
|
||||
label: formatMessage(labels.referrers),
|
||||
key: FILTER_REFERRERS,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
|
||||
label: formatMessage(labels.pages),
|
||||
key: FILTER_PAGES,
|
||||
},
|
||||
];
|
||||
@ -38,7 +40,7 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
||||
);
|
||||
};
|
||||
|
||||
const [referrers, pages] = useMemo(() => {
|
||||
const [referrers = [], pages = []] = useMemo(() => {
|
||||
if (pageviews) {
|
||||
const referrers = percentFilter(
|
||||
pageviews
|
||||
@ -83,24 +85,27 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
||||
|
||||
return [referrers, pages];
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [pageviews]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} />
|
||||
<ButtonGroup items={buttons} selectedKey={filter} onSelect={setFilter}>
|
||||
{({ key, label }) => <Button key={key}>{label}</Button>}
|
||||
</ButtonGroup>
|
||||
{filter === FILTER_REFERRERS && (
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
title={formatMessage(labels.referrers)}
|
||||
metric={formatMessage(labels.views)}
|
||||
renderLabel={renderLink}
|
||||
data={referrers}
|
||||
/>
|
||||
)}
|
||||
{filter === FILTER_PAGES && (
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
title={formatMessage(labels.pages)}
|
||||
metric={formatMessage(labels.views)}
|
||||
renderLabel={renderLink}
|
||||
data={pages}
|
||||
/>
|
@ -8,13 +8,10 @@ import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import useApi from 'hooks/useApi';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import { labels } from 'components/messages';
|
||||
import styles from './WebsiteDetails.module.css';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import WebsiteMenuView from './WebsiteMenuView';
|
||||
|
||||
export default function WebsiteDetails({ websiteId }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['websites', websiteId], () =>
|
||||
get(`/websites/${websiteId}`),
|
||||
@ -22,7 +19,6 @@ export default function WebsiteDetails({ websiteId }) {
|
||||
const [chartLoaded, setChartLoaded] = useState(false);
|
||||
|
||||
const {
|
||||
resolve,
|
||||
query: { view },
|
||||
} = usePageQuery();
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Row, Column, Menu, Item, Icon, Button, Flexbox, Text } from 'react-basics';
|
||||
import { Menu, Item, Icon, Button, Flexbox, Text } from 'react-basics';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Link from 'next/link';
|
||||
import classNames from 'classnames';
|
||||
import { GridRow, GridColumn } from 'components/layout/Grid';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
@ -33,7 +33,7 @@ const views = {
|
||||
export default function WebsiteMenuView({ websiteId, websiteDomain }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
resolve,
|
||||
resolveUrl,
|
||||
query: { view },
|
||||
} = usePageQuery();
|
||||
|
||||
@ -80,12 +80,12 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) {
|
||||
},
|
||||
];
|
||||
|
||||
const DetailsComponent = views[view];
|
||||
const DetailsComponent = views[view] || (() => null);
|
||||
|
||||
return (
|
||||
<Row className={styles.row}>
|
||||
<Column defaultSize={3} className={classNames(styles.col, styles.menu)}>
|
||||
<Link href={resolve({ view: undefined })}>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} defaultSize={3} className={styles.menu}>
|
||||
<Link href={resolveUrl({ view: undefined })}>
|
||||
<a>
|
||||
<Flexbox justifyContent="center">
|
||||
<Button variant="quiet">
|
||||
@ -100,14 +100,14 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) {
|
||||
<Menu items={items} selectedKey={view}>
|
||||
{({ key, label }) => (
|
||||
<Item key={key} className={styles.item}>
|
||||
<Link href={resolve({ view: key })} shallow={true}>
|
||||
<Link href={resolveUrl({ view: key })} shallow={true}>
|
||||
<a>{label}</a>
|
||||
</Link>
|
||||
</Item>
|
||||
)}
|
||||
</Menu>
|
||||
</Column>
|
||||
<Column defaultSize={9} className={classNames(styles.col, styles.data)}>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} sm={12} md={12} defaultSize={9} className={styles.data}>
|
||||
<DetailsComponent
|
||||
websiteId={websiteId}
|
||||
websiteDomain={websiteDomain}
|
||||
@ -117,7 +117,7 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) {
|
||||
showFilters={true}
|
||||
virtualize={true}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,3 @@
|
||||
.row {
|
||||
border-top: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.col {
|
||||
border-left: 1px solid var(--base300);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.col:first-child {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
gap: 20px;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Row, Column } from 'react-basics';
|
||||
import { GridRow, GridColumn } from 'components/layout/Grid';
|
||||
//import { Row as GridRow, Column as GridColumn } from 'react-basics';
|
||||
import PagesTable from 'components/metrics/PagesTable';
|
||||
import ReferrersTable from 'components/metrics/ReferrersTable';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
@ -9,7 +10,6 @@ import WorldMap from 'components/common/WorldMap';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import styles from './WebsiteTableView.module.css';
|
||||
|
||||
export default function WebsiteTableView({ websiteId }) {
|
||||
const [countryData, setCountryData] = useState();
|
||||
@ -20,41 +20,41 @@ export default function WebsiteTableView({ websiteId }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row className={styles.row}>
|
||||
<Column className={styles.col} variant="two">
|
||||
<GridRow>
|
||||
<GridColumn variant="two">
|
||||
<PagesTable {...tableProps} />
|
||||
</Column>
|
||||
<Column className={styles.col} variant="two">
|
||||
</GridColumn>
|
||||
<GridColumn variant="two">
|
||||
<ReferrersTable {...tableProps} />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row className={styles.row}>
|
||||
<Column className={styles.col} variant="three">
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn variant="three">
|
||||
<BrowsersTable {...tableProps} />
|
||||
</Column>
|
||||
<Column className={styles.col} variant="three">
|
||||
</GridColumn>
|
||||
<GridColumn variant="three">
|
||||
<OSTable {...tableProps} />
|
||||
</Column>
|
||||
<Column className={styles.col} variant="three">
|
||||
</GridColumn>
|
||||
<GridColumn variant="three">
|
||||
<DevicesTable {...tableProps} />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row className={styles.row}>
|
||||
<Column className={styles.col} xs={12} sm={12} md={12} defaultSize={8}>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} defaultSize={8}>
|
||||
<WorldMap data={countryData} />
|
||||
</Column>
|
||||
<Column className={styles.col} xs={12} sm={12} md={12} defaultSize={4}>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} sm={12} md={12} defaultSize={4}>
|
||||
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row className={styles.row}>
|
||||
<Column className={styles.col} xs={12} md={12} lg={4} defaultSize={4}>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} lg={4} defaultSize={4}>
|
||||
<EventsTable {...tableProps} />
|
||||
</Column>
|
||||
<Column className={styles.col} xs={12} md={12} lg={8} defaultSize={8}>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} sm={12} md={12} lg={8} defaultSize={8}>
|
||||
<EventsChart websiteId={websiteId} />
|
||||
</Column>
|
||||
</Row>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ export default function usePageQuery() {
|
||||
}, {});
|
||||
}, [search]);
|
||||
|
||||
function resolve(params) {
|
||||
function resolveUrl(params) {
|
||||
return buildUrl(asPath.split('?')[0], { ...query, ...params });
|
||||
}
|
||||
|
||||
return { pathname, query, resolve, router };
|
||||
return { pathname, query, resolveUrl, router };
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { subMinutes } from 'date-fns';
|
||||
import { RealtimeInit } from 'lib/types';
|
||||
import { NextApiRequestAuth } from 'lib/types';
|
||||
import { RealtimeInit, NextApiRequestAuth } from 'lib/types';
|
||||
import { secret } from 'lib/crypto';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { NextApiResponse } from 'next';
|
||||
|
@ -3,9 +3,8 @@ import { useAuth } from 'lib/middleware';
|
||||
import { getRealtimeData } from 'queries';
|
||||
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||
import { secret } from 'lib/crypto';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, RealtimeUpdate } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { RealtimeUpdate } from 'lib/types';
|
||||
|
||||
export interface InitUpdateRequestQuery {
|
||||
startAt: string;
|
||||
|
@ -2,8 +2,12 @@ import create from 'zustand';
|
||||
|
||||
const store = create(() => ({}));
|
||||
|
||||
export function saveQuery(url, data) {
|
||||
store.setState({ [url]: data });
|
||||
export function saveQuery(key, data) {
|
||||
store.setState({ [key]: data });
|
||||
}
|
||||
|
||||
export function getQuery(key) {
|
||||
return store.getState()[key];
|
||||
}
|
||||
|
||||
export default store;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import create from 'zustand';
|
||||
import produce from 'immer';
|
||||
import app from './app';
|
||||
import { getDateRange } from '../lib/date';
|
||||
import { getDateRange } from 'lib/date';
|
||||
|
||||
const store = create(() => ({}));
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user