mirror of
https://github.com/umami-software/umami.git
synced 2026-02-11 16:17:13 +01:00
Updates to realtime. Fixed refresh button.
This commit is contained in:
parent
638a674e99
commit
28921a7cd5
31 changed files with 373 additions and 314 deletions
|
|
@ -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;
|
||||
}
|
||||
187
components/pages/realtime/RealtimeLog.js
Normal file
187
components/pages/realtime/RealtimeLog.js
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { StatusLight, Icon } from 'react-basics';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import firstBy from 'thenby';
|
||||
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 { 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 = 'type-all';
|
||||
const TYPE_PAGEVIEW = 'type-pageview';
|
||||
const TYPE_SESSION = 'type-session';
|
||||
const TYPE_EVENT = 'type-event';
|
||||
|
||||
const TYPE_ICONS = {
|
||||
[TYPE_PAGEVIEW]: <Icons.Eye />,
|
||||
[TYPE_SESSION]: <Icons.Visitor />,
|
||||
[TYPE_EVENT]: <Icons.Bolt />,
|
||||
};
|
||||
|
||||
export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
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) {
|
||||
return logs.filter(row => getType(row) === filter);
|
||||
}
|
||||
return logs;
|
||||
}, [data, filter]);
|
||||
|
||||
const uuids = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.sessions.reduce((obj, { sessionId, sessionUuid }) => {
|
||||
obj[sessionId] = sessionUuid;
|
||||
return obj;
|
||||
}, {});
|
||||
}, [data]);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: formatMessage(labels.all),
|
||||
key: TYPE_ALL,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.views),
|
||||
key: TYPE_PAGEVIEW,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.sessions),
|
||||
key: TYPE_SESSION,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.events),
|
||||
key: TYPE_EVENT,
|
||||
},
|
||||
];
|
||||
|
||||
function getType({ pageviewId, sessionId, eventId }) {
|
||||
if (eventId) {
|
||||
return TYPE_EVENT;
|
||||
}
|
||||
if (pageviewId) {
|
||||
return TYPE_PAGEVIEW;
|
||||
}
|
||||
if (sessionId) {
|
||||
return TYPE_SESSION;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getIcon(row) {
|
||||
return TYPE_ICONS[getType(row)];
|
||||
}
|
||||
|
||||
function getWebsite({ websiteId }) {
|
||||
return websites.find(n => n.id === websiteId);
|
||||
}
|
||||
|
||||
function getDetail({
|
||||
eventName,
|
||||
pageviewId,
|
||||
sessionId,
|
||||
url,
|
||||
browser,
|
||||
os,
|
||||
country,
|
||||
device,
|
||||
websiteId,
|
||||
}) {
|
||||
if (eventName) {
|
||||
return <div>{eventName}</div>;
|
||||
}
|
||||
if (pageviewId) {
|
||||
const domain = getWebsite({ websiteId })?.domain;
|
||||
return (
|
||||
<a
|
||||
className={styles.link}
|
||||
href={`//${domain}${url}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{safeDecodeURI(url)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
if (sessionId) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="message.log.visitor"
|
||||
defaultMessage="Visitor from {country} using {browser} on {os} {device}"
|
||||
values={{
|
||||
country: <b>{countryNames[country] || formatMessage(labels.unknown)}</b>,
|
||||
browser: <b>{BROWSERS[browser]}</b>,
|
||||
os: <b>{os}</b>,
|
||||
device: <b>{formatMessage(getDeviceMessage(device))}</b>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getTime({ createdAt }) {
|
||||
return dateFormat(new Date(createdAt), 'pp', locale);
|
||||
}
|
||||
|
||||
function getColor(row) {
|
||||
const { sessionId } = row;
|
||||
|
||||
return stringToColor(uuids[sessionId] || `${sessionId}${getWebsite(row)}`);
|
||||
}
|
||||
|
||||
const Row = ({ index, style }) => {
|
||||
const row = logs[index];
|
||||
return (
|
||||
<div className={styles.row} style={style}>
|
||||
<div>
|
||||
<StatusLight color={getColor(row)} />
|
||||
</div>
|
||||
<div className={styles.time}>{getTime(row)}</div>
|
||||
<div className={styles.detail}>
|
||||
<Icon className={styles.icon} icon={getIcon(row)} />
|
||||
{getDetail(row)}
|
||||
</div>
|
||||
{!websiteId && websites.length > 1 && (
|
||||
<div className={styles.website}>{getWebsite(row)?.domain}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
<FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} />
|
||||
<div className={styles.header}>
|
||||
<FormattedMessage id="label.realtime-logs" defaultMessage="Realtime logs" />
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{logs?.length === 0 && <NoData />}
|
||||
{logs?.length > 0 && (
|
||||
<FixedSizeList height={400} itemCount={logs.length} itemSize={40}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
components/pages/realtime/RealtimeLog.module.css
Normal file
59
components/pages/realtime/RealtimeLog.module.css
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
.table {
|
||||
font-size: var(--font-size-xs);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: fit-content(100%) fit-content(100%) auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
line-height: 40px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.time {
|
||||
min-width: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.website {
|
||||
text-align: right;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row .link {
|
||||
color: var(--base900);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.row .link:hover {
|
||||
color: var(--primary400);
|
||||
}
|
||||
115
components/pages/realtime/RealtimeViews.js
Normal file
115
components/pages/realtime/RealtimeViews.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { useMemo, useState, useCallback } from 'react';
|
||||
import { ButtonGroup, Button } from 'react-basics';
|
||||
import { useIntl } from 'react-intl';
|
||||
import firstBy from 'thenby';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
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 }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { pageviews } = data;
|
||||
const [filter, setFilter] = useState(FILTER_REFERRERS);
|
||||
const domains = useMemo(() => websites.map(({ domain }) => domain), [websites]);
|
||||
const getDomain = useCallback(
|
||||
id =>
|
||||
websites.length === 1
|
||||
? websites[0]?.domain
|
||||
: websites.find(({ websiteId }) => websiteId === id)?.domain,
|
||||
[websites],
|
||||
);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: formatMessage(labels.referrers),
|
||||
key: FILTER_REFERRERS,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.pages),
|
||||
key: FILTER_PAGES,
|
||||
},
|
||||
];
|
||||
|
||||
const renderLink = ({ x }) => {
|
||||
const domain = x.startsWith('/') ? getDomain(websiteId) : '';
|
||||
return (
|
||||
<a href={`//${domain}${x}`} target="_blank" rel="noreferrer noopener">
|
||||
{x}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const [referrers = [], pages = []] = useMemo(() => {
|
||||
if (pageviews) {
|
||||
const referrers = percentFilter(
|
||||
pageviews
|
||||
.reduce((arr, { referrer }) => {
|
||||
if (referrer?.startsWith('http')) {
|
||||
const hostname = new URL(referrer).hostname.replace(/^www\./, '');
|
||||
|
||||
if (hostname && !domains.includes(hostname)) {
|
||||
const row = arr.find(({ x }) => x === hostname);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: hostname, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(firstBy('y', -1)),
|
||||
);
|
||||
|
||||
const pages = percentFilter(
|
||||
pageviews
|
||||
.reduce((arr, { url, websiteId }) => {
|
||||
if (url?.startsWith('/')) {
|
||||
if (!websiteId && websites.length > 1) {
|
||||
url = `${getDomain(websiteId)}${url}`;
|
||||
}
|
||||
const row = arr.find(({ x }) => x === url);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: url, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(firstBy('y', -1)),
|
||||
);
|
||||
|
||||
return [referrers, pages];
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [pageviews]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup items={buttons} selectedKey={filter} onSelect={setFilter}>
|
||||
{({ key, label }) => <Button key={key}>{label}</Button>}
|
||||
</ButtonGroup>
|
||||
{filter === FILTER_REFERRERS && (
|
||||
<DataTable
|
||||
title={formatMessage(labels.referrers)}
|
||||
metric={formatMessage(labels.views)}
|
||||
renderLabel={renderLink}
|
||||
data={referrers}
|
||||
/>
|
||||
)}
|
||||
{filter === FILTER_PAGES && (
|
||||
<DataTable
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue