mirror of
https://github.com/umami-software/umami.git
synced 2026-02-19 20:15:41 +01:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
a20e3483e0
242 changed files with 18823 additions and 3413 deletions
29
components/common/ConfirmDeleteForm.js
Normal file
29
components/common/ConfirmDeleteForm.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, LoadingButton, Form, FormButtons } from 'react-basics';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export function ConfirmDeleteForm({ name, onConfirm, onClose }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
|
||||
const handleConfirm = () => {
|
||||
setLoading(true);
|
||||
onConfirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<p>
|
||||
<FormattedMessage {...messages.confirmDelete} values={{ target: <b>{name}</b> }} />
|
||||
</p>
|
||||
<FormButtons flex>
|
||||
<LoadingButton loading={loading} onClick={handleConfirm} variant="danger">
|
||||
{formatMessage(labels.delete)}
|
||||
</LoadingButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmDeleteForm;
|
||||
|
|
@ -12,7 +12,6 @@ export function ErrorBoundary({ children }) {
|
|||
const { formatMessage, messages } = useMessages();
|
||||
|
||||
const fallbackRender = ({ error, resetErrorBoundary }) => {
|
||||
console.log({ error });
|
||||
return (
|
||||
<div className={styles.error} role="alert">
|
||||
<h1>{formatMessage(messages.error)}</h1>
|
||||
|
|
|
|||
12
components/common/LinkButton.js
Normal file
12
components/common/LinkButton.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import Link from 'next/link';
|
||||
import { Icon, Icons, Text } from 'react-basics';
|
||||
import styles from './LinkButton.module.css';
|
||||
|
||||
export default function LinkButton({ href, icon, children }) {
|
||||
return (
|
||||
<Link className={styles.button} href={href}>
|
||||
<Icon>{icon || <Icons.ArrowRight />}</Icon>
|
||||
<Text>{children}</Text>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
28
components/common/LinkButton.module.css
Normal file
28
components/common/LinkButton.module.css
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
white-space: nowrap;
|
||||
gap: var(--size200);
|
||||
font-family: inherit;
|
||||
color: var(--base900);
|
||||
background: var(--base100);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--border-radius);
|
||||
min-height: var(--base-height);
|
||||
padding: 0 var(--size600);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--base200);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
background: var(--base300);
|
||||
}
|
||||
|
||||
.button:visited {
|
||||
color: var(--base900);
|
||||
}
|
||||
|
|
@ -1,15 +1,23 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { Button, Row, Column } from 'react-basics';
|
||||
import { setItem } from 'next-basics';
|
||||
import useStore, { checkVersion } from 'store/version';
|
||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||
import styles from './UpdateNotice.module.css';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export function UpdateNotice() {
|
||||
export function UpdateNotice({ user, config }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { latest, checked, hasUpdate, releaseUrl } = useStore();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const { pathname } = useRouter();
|
||||
const [dismissed, setDismissed] = useState(checked);
|
||||
const allowUpdate =
|
||||
user?.isAdmin &&
|
||||
!config?.updatesDisabled &&
|
||||
!config?.cloudMode &&
|
||||
!pathname.includes('/share/') &&
|
||||
!dismissed;
|
||||
|
||||
const updateCheck = useCallback(() => {
|
||||
setItem(VERSION_CHECK, { version: latest, time: Date.now() });
|
||||
|
|
@ -27,12 +35,12 @@ export function UpdateNotice() {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!checked) {
|
||||
if (allowUpdate) {
|
||||
checkVersion();
|
||||
}
|
||||
}, [checked]);
|
||||
}, [allowUpdate]);
|
||||
|
||||
if (!hasUpdate || dismissed) {
|
||||
if (!allowUpdate || !hasUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
gap: 20px;
|
||||
margin: 20px auto;
|
||||
justify-self: center;
|
||||
background: #fff;
|
||||
background: var(--base50);
|
||||
padding: 20px;
|
||||
border: 1px solid var(--base300);
|
||||
border-radius: var(--border-radius);
|
||||
|
|
@ -15,7 +15,8 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: var(--font-color100);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { Icons } from 'react-basics';
|
||||
import AddUser from 'assets/add-user.svg';
|
||||
import Bars from 'assets/bars.svg';
|
||||
import BarChart from 'assets/bar-chart.svg';
|
||||
import Bolt from 'assets/bolt.svg';
|
||||
import Calendar from 'assets/calendar.svg';
|
||||
import Clock from 'assets/clock.svg';
|
||||
|
|
@ -22,6 +24,8 @@ import Visitor from 'assets/visitor.svg';
|
|||
const icons = {
|
||||
...Icons,
|
||||
AddUser,
|
||||
Bars,
|
||||
BarChart,
|
||||
Bolt,
|
||||
Calendar,
|
||||
Clock,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ export function RefreshButton({ websiteId, isLoading }) {
|
|||
|
||||
function handleClick() {
|
||||
if (!isLoading && dateRange) {
|
||||
if (/^\d+/.test(dateRange.value)) {
|
||||
setWebsiteDateRange(websiteId, dateRange.value);
|
||||
} else {
|
||||
setWebsiteDateRange(websiteId, dateRange);
|
||||
}
|
||||
setWebsiteDateRange(websiteId, dateRange);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,13 @@
|
|||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import DateFilter from './DateFilter';
|
||||
import styles from './WebsiteDateFilter.module.css';
|
||||
|
||||
export default function WebsiteDateFilter({ websiteId }) {
|
||||
const { get } = useApi();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { value, startDate, endDate } = dateRange;
|
||||
|
||||
const handleChange = async value => {
|
||||
if (value === 'all' && websiteId) {
|
||||
const data = await get(`/websites/${websiteId}`);
|
||||
|
||||
if (data) {
|
||||
const start = new Date(data.createdAt).getTime();
|
||||
const end = Date.now();
|
||||
setDateRange(`range:${start}:${end}`);
|
||||
}
|
||||
} else if (value !== 'all') {
|
||||
setDateRange(value);
|
||||
}
|
||||
setDateRange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Container } from 'react-basics';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import NavBar from 'components/layout/NavBar';
|
||||
import UpdateNotice from 'components/common/UpdateNotice';
|
||||
import useRequireLogin from 'hooks/useRequireLogin';
|
||||
|
|
@ -11,17 +10,14 @@ import styles from './AppLayout.module.css';
|
|||
export function AppLayout({ title, children }) {
|
||||
const { user } = useRequireLogin();
|
||||
const config = useConfig();
|
||||
const { pathname } = useRouter();
|
||||
|
||||
if (!user || !config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allowUpdate = user?.isAdmin && !config?.updatesDisabled && !pathname.includes('/share/');
|
||||
|
||||
return (
|
||||
<div className={styles.layout} data-app-version={CURRENT_VERSION}>
|
||||
{allowUpdate && <UpdateNotice />}
|
||||
<UpdateNotice user={user} config={config} />
|
||||
<Head>
|
||||
<title>{title ? `${title} | Triton Analytics` : 'Triton'}</title>
|
||||
</Head>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
.nav {
|
||||
height: 60px;
|
||||
width: 100vw;
|
||||
z-index: var(--z-index-overlay);
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
|
@ -19,4 +18,5 @@
|
|||
min-height: 0;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,12 @@
|
|||
import { Row, Column } from 'react-basics';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { CURRENT_VERSION, HOMEPAGE_URL, REPO_URL } from 'lib/constants';
|
||||
import { labels } from 'components/messages';
|
||||
import { CURRENT_VERSION, HOMEPAGE_URL } from 'lib/constants';
|
||||
import styles from './Footer.module.css';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<Row>
|
||||
<Column defaultSize={12} lg={11} xl={11}>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
{...labels.poweredBy}
|
||||
values={{
|
||||
name: (
|
||||
<a href={HOMEPAGE_URL}>
|
||||
<b>umami</b>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
<Column className={styles.version} defaultSize={12} lg={1} xl={1}>
|
||||
<a href={REPO_URL}>{`v${CURRENT_VERSION}`}</a>
|
||||
</Column>
|
||||
</Row>
|
||||
<a href={HOMEPAGE_URL}>
|
||||
<b>umami</b> {`v${CURRENT_VERSION}`}
|
||||
</a>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
font-size: var(--font-size-sm);
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
margin: 60px 0;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--font-color100);
|
||||
}
|
||||
|
||||
.version {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import styles from './Header.module.css';
|
|||
export function Header() {
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<Row>
|
||||
<Row className={styles.row}>
|
||||
<Column>
|
||||
<Link href="https://tritoncg.com" target="_blank" className={styles.title}>
|
||||
<Icon size="lg">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 30px 30px 0 30px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
@ -35,18 +40,8 @@
|
|||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.buttons,
|
||||
.links {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,4 @@
|
|||
flex-direction: column;
|
||||
background: var(--base50);
|
||||
position: relative;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.page {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
align-self: stretch;
|
||||
flex-wrap: wrap;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.header a {
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export const labels = defineMessages({
|
|||
devices: { id: 'label.devices', defaultMessage: 'Devices' },
|
||||
countries: { id: 'label.countries', defaultMessage: 'Countries' },
|
||||
languages: { id: 'label.languages', defaultMessage: 'Languages' },
|
||||
event: { id: 'label.event', defaultMessage: 'Event' },
|
||||
events: { id: 'label.events', defaultMessage: 'Events' },
|
||||
query: { id: 'label.query', defaultMessage: 'Query' },
|
||||
queryParameters: { id: 'label.query-parameters', defaultMessage: 'Query parameters' },
|
||||
|
|
@ -159,6 +160,8 @@ export const labels = defineMessages({
|
|||
value: { id: 'labels.value', defaultMessage: 'Value' },
|
||||
overview: { id: 'labels.overview', defaultMessage: 'Overview' },
|
||||
totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' },
|
||||
insights: { id: 'label.insights', defaultMessage: 'Insights' },
|
||||
dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
@ -226,19 +229,19 @@ export const messages = defineMessages({
|
|||
defaultMessage: 'All website data will be deleted.',
|
||||
},
|
||||
noResultsFound: {
|
||||
id: 'messages.no-results-found',
|
||||
id: 'message.no-results-found',
|
||||
defaultMessage: 'No results were found.',
|
||||
},
|
||||
noWebsitesConfigured: {
|
||||
id: 'messages.no-websites-configured',
|
||||
id: 'message.no-websites-configured',
|
||||
defaultMessage: 'You do not have any websites configured.',
|
||||
},
|
||||
noTeamWebsites: {
|
||||
id: 'messages.no-team-websites',
|
||||
id: 'message.no-team-websites',
|
||||
defaultMessage: 'This team does not have any websites.',
|
||||
},
|
||||
teamWebsitesInfo: {
|
||||
id: 'messages.team-websites-info',
|
||||
id: 'message.team-websites-info',
|
||||
defaultMessage: 'Websites can be viewed by anyone on the team.',
|
||||
},
|
||||
noMatchPassword: { id: 'message.no-match-password', defaultMessage: 'Passwords do not match.' },
|
||||
|
|
@ -270,4 +273,8 @@ export const messages = defineMessages({
|
|||
id: 'message.no-event-data',
|
||||
defaultMessage: 'No event data is available.',
|
||||
},
|
||||
newVersionAvailable: {
|
||||
id: 'message.new-version-available',
|
||||
defaultMessage: 'A new version of Umami {version} is available!',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<StatusLight variant="success">
|
||||
<StatusLight className={styles.container} variant="success">
|
||||
<div className={styles.text}>{formatMessage(messages.activeUsers, { x: count })}</div>
|
||||
</StatusLight>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.value {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import BarChart from './BarChart';
|
|||
import { useLocale, useTheme, useMessages } from 'hooks';
|
||||
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
|
||||
|
||||
export function PageviewsChart({ websiteId, data, unit, className, loading, ...props }) {
|
||||
export function PageviewsChart({ websiteId, data, unit, loading, ...props }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { colors } = useTheme();
|
||||
const { locale } = useLocale();
|
||||
|
|
@ -31,7 +31,6 @@ export function PageviewsChart({ websiteId, data, unit, className, loading, ...p
|
|||
<BarChart
|
||||
{...props}
|
||||
key={websiteId}
|
||||
className={className}
|
||||
datasets={datasets}
|
||||
unit={unit}
|
||||
loading={loading}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export function Dashboard({ userId }) {
|
|||
const { showCharts, limit, editing } = dashboard;
|
||||
const [max, setMax] = useState(limit);
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['websites'], () => get('/websites', { userId }));
|
||||
const { data, isLoading, error } = useQuery(['websites'], () =>
|
||||
get('/websites', { userId, includeTeams: 1 }),
|
||||
);
|
||||
const hasData = data && data.length !== 0;
|
||||
const { dir } = useLocale();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Menu, Icon, Text, PopupTrigger, Popup, Item, Button } from 'react-basics';
|
||||
import { TooltipPopup, Icon, Text, Flexbox, Popup, Item, Button } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import { saveDashboard } from 'store/dashboard';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
|
@ -6,40 +6,30 @@ import useMessages from 'hooks/useMessages';
|
|||
export function DashboardSettingsButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
label: formatMessage(labels.toggleCharts),
|
||||
value: 'charts',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.editDashboard),
|
||||
value: 'order',
|
||||
},
|
||||
];
|
||||
const handleToggleCharts = () => {
|
||||
saveDashboard(state => ({ showCharts: !state.showCharts }));
|
||||
};
|
||||
|
||||
function handleSelect(value) {
|
||||
if (value === 'charts') {
|
||||
saveDashboard(state => ({ showCharts: !state.showCharts }));
|
||||
}
|
||||
if (value === 'order') {
|
||||
saveDashboard({ editing: true });
|
||||
}
|
||||
}
|
||||
const handleEdit = () => {
|
||||
saveDashboard({ editing: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button>
|
||||
<Flexbox gap={10}>
|
||||
<TooltipPopup label={formatMessage(labels.toggleCharts)} position="bottom">
|
||||
<Button onClick={handleToggleCharts}>
|
||||
<Icon>
|
||||
<Icons.BarChart />
|
||||
</Icon>
|
||||
</Button>
|
||||
</TooltipPopup>
|
||||
<Button onClick={handleEdit}>
|
||||
<Icon>
|
||||
<Icons.Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</Button>
|
||||
<Popup alignment="end">
|
||||
<Menu variant="popup" items={menuOptions} onSelect={handleSelect}>
|
||||
{({ label, value }) => <Item key={value}>{label}</Item>}
|
||||
</Menu>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,15 @@ export function EventDataTable({ data = [] }) {
|
|||
|
||||
return (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="event" label={formatMessage(labels.event)}>
|
||||
{row => (
|
||||
<Link href={resolveUrl({ event: row.event })} shallow={true}>
|
||||
{row.event}
|
||||
</Link>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="field" label={formatMessage(labels.field)}>
|
||||
{row => {
|
||||
return (
|
||||
<Link href={resolveUrl({ view: row.field })} shallow={true}>
|
||||
{row.field}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
{row => row.field}
|
||||
</GridColumn>
|
||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)}>
|
||||
{({ total }) => total.toLocaleString()}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ import Icons from 'components/icons';
|
|||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Empty from 'components/common/Empty';
|
||||
|
||||
export function EventDataTable({ data = [], field }) {
|
||||
export function EventDataValueTable({ data = [], event }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { resolveUrl } = usePageQuery();
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<>
|
||||
<Link href={resolveUrl({ view: undefined })}>
|
||||
<Link href={resolveUrl({ event: undefined })}>
|
||||
<Button>
|
||||
<Icon rotate={180}>
|
||||
<Icons.ArrowRight />
|
||||
|
|
@ -20,7 +20,7 @@ export function EventDataTable({ data = [], field }) {
|
|||
<Text>{formatMessage(labels.back)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
<Text>{field}</Text>
|
||||
<Text>{event}</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -31,6 +31,7 @@ export function EventDataTable({ data = [], field }) {
|
|||
{data.length <= 0 && <Empty />}
|
||||
{data.length > 0 && (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="field" label={formatMessage(labels.field)} />
|
||||
<GridColumn name="value" label={formatMessage(labels.value)} />
|
||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)} width="200px">
|
||||
{({ total }) => total.toLocaleString()}
|
||||
|
|
@ -41,4 +42,4 @@ export function EventDataTable({ data = [], field }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default EventDataTable;
|
||||
export default EventDataValueTable;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import DataTable from 'components/metrics/DataTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import classNames from 'classnames';
|
||||
import styles from './RealtimeCountries.module.css';
|
||||
|
||||
export function RealtimeCountries({ data }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { basePath } = useRouter();
|
||||
|
||||
const renderCountryName = useCallback(
|
||||
({ x }) => <span className={locale}>{countryNames[x]}</span>,
|
||||
[countryNames, locale],
|
||||
({ x: code }) => (
|
||||
<span className={classNames(locale, styles.row)}>
|
||||
<img src={`${basePath}/images/flags/${code?.toLowerCase() || 'xx'}.png`} alt={code} />
|
||||
{countryNames[code]}
|
||||
</span>
|
||||
),
|
||||
[countryNames, locale, basePath],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
5
components/pages/realtime/RealtimeCountries.module.css
Normal file
5
components/pages/realtime/RealtimeCountries.module.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
|||
|
||||
const getTime = ({ createdAt }) => dateFormat(new Date(createdAt), 'pp', locale);
|
||||
|
||||
const getColor = ({ sessionId }) => stringToColor(sessionId);
|
||||
const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
|
||||
|
||||
const getIcon = ({ __type }) => icons[__type];
|
||||
|
||||
|
|
|
|||
|
|
@ -93,9 +93,7 @@ export function RealtimePage({ websiteId }) {
|
|||
<Page loading={isLoading} error={error}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<RealtimeHeader websiteId={websiteId} data={currentData} />
|
||||
<div className={styles.chart}>
|
||||
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
|
||||
</div>
|
||||
<RealtimeChart className={styles.chart} data={realtimeData} unit="minute" />
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} lg={4} xl={4}>
|
||||
<RealtimeUrls websiteId={websiteId} websiteDomain={website?.domain} data={realtimeData} />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { REPORT_PARAMETERS } from 'lib/constants';
|
||||
import PopupForm from '../PopupForm';
|
||||
import FieldSelectForm from '../FieldSelectForm';
|
||||
import FieldAggregateForm from '../FieldAggregateForm';
|
||||
import FieldFilterForm from '../FieldFilterForm';
|
||||
import PopupForm from './PopupForm';
|
||||
import FieldSelectForm from './FieldSelectForm';
|
||||
import FieldAggregateForm from './FieldAggregateForm';
|
||||
import FieldFilterForm from './FieldFilterForm';
|
||||
import styles from './FieldAddForm.module.css';
|
||||
|
||||
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
||||
|
|
@ -19,6 +19,10 @@ export default function FieldAggregateForm({ name, type, onSelect }) {
|
|||
{ label: formatMessage(labels.total), value: 'total' },
|
||||
{ label: formatMessage(labels.unique), value: 'unique' },
|
||||
],
|
||||
uuid: [
|
||||
{ label: formatMessage(labels.total), value: 'total' },
|
||||
{ label: formatMessage(labels.unique), value: 'unique' },
|
||||
],
|
||||
};
|
||||
|
||||
const items = options[type];
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ export default function FieldSelectForm({ fields, onSelect }) {
|
|||
<Form>
|
||||
<FormRow label={formatMessage(labels.fields)}>
|
||||
<Menu className={styles.menu} onSelect={key => onSelect(fields[key])}>
|
||||
{fields.map(({ name, type }, index) => {
|
||||
{fields.map(({ label, name, type }, index) => {
|
||||
return (
|
||||
<Item key={index} className={styles.item}>
|
||||
<div>{name}</div>
|
||||
<div>{label || name}</div>
|
||||
<div className={styles.type}>{type}</div>
|
||||
</Item>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ export const ReportContext = createContext(null);
|
|||
export function Report({ reportId, defaultParameters, children, ...props }) {
|
||||
const report = useReport(reportId, defaultParameters);
|
||||
|
||||
//console.log({ report });
|
||||
|
||||
return (
|
||||
<ReportContext.Provider value={{ ...report }}>
|
||||
<Page {...props} className={styles.container}>
|
||||
|
|
|
|||
|
|
@ -3,20 +3,10 @@ import { Button, Icons, Text, Icon } from 'react-basics';
|
|||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
import styles from './ReportTemplates.module.css';
|
||||
import { useMessages } from 'hooks';
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: 'Funnel',
|
||||
description: 'Understand the conversion and drop-off rate of users.',
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
];
|
||||
|
||||
function ReportItem({ title, description, url, icon }) {
|
||||
return (
|
||||
<div className={styles.report}>
|
||||
|
|
@ -42,6 +32,23 @@ function ReportItem({ title, description, url, icon }) {
|
|||
export function ReportTemplates() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const reports = [
|
||||
/*
|
||||
{
|
||||
title: formatMessage(labels.insights),
|
||||
description: 'Dive deeper into your data by using segments and filters.',
|
||||
url: '/reports/insights',
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
*/
|
||||
{
|
||||
title: formatMessage(labels.funnel),
|
||||
description: 'Understand the conversion and drop-off rate of users.',
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader title={formatMessage(labels.reports)} />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import Link from 'next/link';
|
||||
import { Button, Text, Icon, Icons } from 'react-basics';
|
||||
import { useState } from 'react';
|
||||
import { Flexbox, Icon, Icons, Text, Button, Modal } from 'react-basics';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
|
||||
import { useMessages } from 'hooks';
|
||||
|
||||
export function ReportsTable({ data = [] }) {
|
||||
export function ReportsTable({ data = [], onDelete = () => {} }) {
|
||||
const [report, setReport] = useState(null);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const columns = [
|
||||
|
|
@ -13,23 +16,39 @@ export function ReportsTable({ data = [] }) {
|
|||
{ name: 'action', label: ' ' },
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsTable columns={columns} data={data}>
|
||||
{row => {
|
||||
const { id } = row;
|
||||
const handleConfirm = () => {
|
||||
onDelete(report.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={`/reports/${id}`}>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.view)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
</SettingsTable>
|
||||
return (
|
||||
<>
|
||||
<SettingsTable columns={columns} data={data}>
|
||||
{row => {
|
||||
const { id } = row;
|
||||
|
||||
return (
|
||||
<Flexbox gap={10}>
|
||||
<LinkButton href={`/reports/${id}`}>{formatMessage(labels.view)}</LinkButton>
|
||||
<Button onClick={() => setReport(row)}>
|
||||
<Icon>
|
||||
<Icons.Trash />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
</Flexbox>
|
||||
);
|
||||
}}
|
||||
</SettingsTable>
|
||||
{report && (
|
||||
<Modal>
|
||||
<ConfirmDeleteForm
|
||||
name={report.name}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={() => setReport(null)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ReportContext } from 'components/pages/reports/Report';
|
|||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import Icons from 'components/icons';
|
||||
import FieldAddForm from './FieldAddForm';
|
||||
import FieldAddForm from '../FieldAddForm';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import ParameterList from '../ParameterList';
|
||||
import styles from './EventDataParameters.module.css';
|
||||
|
|
@ -54,9 +54,11 @@ export function EventDataParameters() {
|
|||
};
|
||||
|
||||
const handleAdd = (group, value) => {
|
||||
const data = parameterData[group].filter(({ name }) => name !== value.name);
|
||||
const data = parameterData[group];
|
||||
|
||||
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||
if (!data.find(({ name }) => name === value.name)) {
|
||||
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (group, index) => {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ import { ReportContext } from '../Report';
|
|||
export function FunnelTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data={report?.data}
|
||||
title={formatMessage(labels.url)}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
showPercentage={false}
|
||||
showPercentage={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { REPORT_PARAMETERS } from 'lib/constants';
|
||||
import PopupForm from '../PopupForm';
|
||||
import FieldSelectForm from '../FieldSelectForm';
|
||||
import FieldAggregateForm from '../FieldAggregateForm';
|
||||
import FieldFilterForm from '../FieldFilterForm';
|
||||
import styles from './FieldAddForm.module.css';
|
||||
|
||||
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
||||
const [selected, setSelected] = useState();
|
||||
|
||||
const handleSelect = value => {
|
||||
const { type } = value;
|
||||
|
||||
if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
|
||||
value.value = group === REPORT_PARAMETERS.groups ? '' : 'total';
|
||||
handleSave(value);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelected(value);
|
||||
};
|
||||
|
||||
const handleSave = value => {
|
||||
onAdd(group, value);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<PopupForm className={styles.popup} element={element} onClose={onClose}>
|
||||
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
||||
{selected && group === REPORT_PARAMETERS.fields && (
|
||||
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
||||
)}
|
||||
{selected && group === REPORT_PARAMETERS.filters && (
|
||||
<FieldFilterForm {...selected} onSelect={handleSave} />
|
||||
)}
|
||||
</PopupForm>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldAddForm;
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
.menu {
|
||||
width: 360px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.type {
|
||||
color: var(--font-color300);
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 60px;
|
||||
}
|
||||
|
|
@ -1,42 +1,22 @@
|
|||
import { useContext, useRef } from 'react';
|
||||
import { useApi, useMessages } from 'hooks';
|
||||
import { useMessages } from 'hooks';
|
||||
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import { REPORT_PARAMETERS, WEBSITE_EVENT_FIELDS } from 'lib/constants';
|
||||
import Icons from 'components/icons';
|
||||
import FieldAddForm from './FieldAddForm';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import FieldAddForm from '../FieldAddForm';
|
||||
import ParameterList from '../ParameterList';
|
||||
import styles from './InsightsParameters.module.css';
|
||||
|
||||
function useFields(websiteId, startDate, endDate) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['fields', websiteId, startDate, endDate],
|
||||
() =>
|
||||
get('/reports/event-data', {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
}),
|
||||
{ enabled: !!(websiteId && startDate && endDate) },
|
||||
);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function InsightsParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const ref = useRef(null);
|
||||
const { parameters } = report || {};
|
||||
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
|
||||
const { startDate, endDate } = dateRange || {};
|
||||
const queryEnabled = websiteId && dateRange && fields?.length;
|
||||
const { data, error } = useFields(websiteId, startDate, endDate);
|
||||
const parametersSelected = websiteId && startDate && endDate;
|
||||
const hasData = data?.length !== 0;
|
||||
const fieldOptions = Object.keys(WEBSITE_EVENT_FIELDS).map(key => WEBSITE_EVENT_FIELDS[key]);
|
||||
|
||||
const parameterGroups = [
|
||||
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
||||
|
|
@ -78,10 +58,7 @@ export function InsightsParameters() {
|
|||
{(close, element) => {
|
||||
return (
|
||||
<FieldAddForm
|
||||
fields={data.map(({ eventKey, InsightsType }) => ({
|
||||
name: eventKey,
|
||||
type: DATA_TYPES[InsightsType],
|
||||
}))}
|
||||
fields={fieldOptions}
|
||||
group={group}
|
||||
element={element}
|
||||
onAdd={handleAdd}
|
||||
|
|
@ -95,50 +72,43 @@ export function InsightsParameters() {
|
|||
};
|
||||
|
||||
return (
|
||||
<Form ref={ref} values={parameters} error={error} onSubmit={handleSubmit}>
|
||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
|
||||
<BaseParameters />
|
||||
{!hasData && <Empty message={formatMessage(messages.noInsights)} />}
|
||||
{parametersSelected &&
|
||||
hasData &&
|
||||
parameterGroups.map(({ label, group }) => {
|
||||
return (
|
||||
<FormRow
|
||||
key={label}
|
||||
label={label}
|
||||
action={<AddButton group={group} onAdd={handleAdd} />}
|
||||
{parameterGroups.map(({ label, group }) => {
|
||||
return (
|
||||
<FormRow key={label} label={label} action={<AddButton group={group} onAdd={handleAdd} />}>
|
||||
<ParameterList
|
||||
items={parameterData[group]}
|
||||
onRemove={index => handleRemove(group, index)}
|
||||
>
|
||||
<ParameterList
|
||||
items={parameterData[group]}
|
||||
onRemove={index => handleRemove(group, index)}
|
||||
>
|
||||
{({ name, value }) => {
|
||||
return (
|
||||
<div className={styles.parameter}>
|
||||
{group === REPORT_PARAMETERS.fields && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.filters && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value[0]}</div>
|
||||
<div>{value[1]}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.groups && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
);
|
||||
})}
|
||||
{({ name, value }) => {
|
||||
return (
|
||||
<div className={styles.parameter}>
|
||||
{group === REPORT_PARAMETERS.fields && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.filters && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value[0]}</div>
|
||||
<div>{value[1]}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.groups && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
);
|
||||
})}
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={!queryEnabled} loading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from 'react-basics';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getRandomChars } from 'next-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
|
|
@ -20,12 +21,16 @@ export function ShareUrl({ websiteId, data, onSave }) {
|
|||
const { name, shareId } = data;
|
||||
const [id, setId] = useState(shareId);
|
||||
const { post, useMutation } = useApi();
|
||||
const { basePath } = useRouter();
|
||||
const { mutate, error } = useMutation(({ shareId }) =>
|
||||
post(`/websites/${websiteId}`, { shareId }),
|
||||
);
|
||||
const ref = useRef(null);
|
||||
const url = useMemo(
|
||||
() => `${process.env.analyticsUrl || location.origin}/share/${id}/${encodeURIComponent(name)}`,
|
||||
() =>
|
||||
`${process.env.analyticsUrl || location.origin}${basePath}/share/${id}/${encodeURIComponent(
|
||||
name,
|
||||
)}`,
|
||||
[id, name],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import PageviewsChart from 'components/metrics/PageviewsChart';
|
||||
import { useApi, useDateRange, useTimezone, usePageQuery } from 'hooks';
|
||||
import { getDateArray, getDateLength } from 'lib/date';
|
||||
import { getDateArray } from 'lib/date';
|
||||
|
||||
export function WebsiteChart({ websiteId }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
|
|
@ -43,17 +43,9 @@ export function WebsiteChart({ websiteId }) {
|
|||
};
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data, startDate, endDate, unit, modified]);
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
return (
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
loading={isLoading}
|
||||
/>
|
||||
);
|
||||
return <PageviewsChart websiteId={websiteId} data={chartData} unit={unit} loading={isLoading} />;
|
||||
}
|
||||
|
||||
export default WebsiteChart;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
|
|||
</Link>
|
||||
</WebsiteHeader>
|
||||
<WebsiteMetricsBar websiteId={id} />
|
||||
<WebsiteChart websiteId={id} showChart={showCharts} />
|
||||
{showCharts && <WebsiteChart websiteId={id} />}
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,18 @@ import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetric
|
|||
import { useDateRange, useApi, usePageQuery } from 'hooks';
|
||||
import styles from './WebsiteEventData.module.css';
|
||||
|
||||
function useFields(websiteId, field) {
|
||||
function useData(websiteId, event) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['event-data:fields', { websiteId, startDate, endDate, field }],
|
||||
['event-data:events', { websiteId, startDate, endDate, event }],
|
||||
() =>
|
||||
get('/event-data/fields', {
|
||||
get('/event-data/events', {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
field,
|
||||
event,
|
||||
}),
|
||||
{ enabled: !!(websiteId && startDate && endDate) },
|
||||
);
|
||||
|
|
@ -26,15 +26,15 @@ function useFields(websiteId, field) {
|
|||
|
||||
export default function WebsiteEventData({ websiteId }) {
|
||||
const {
|
||||
query: { view },
|
||||
query: { event },
|
||||
} = usePageQuery();
|
||||
const { data } = useFields(websiteId, view);
|
||||
const { data } = useData(websiteId, event);
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container} direction="column" gap={20}>
|
||||
<EventDataMetricsBar websiteId={websiteId} />
|
||||
{!view && <EventDataTable data={data} />}
|
||||
{view && <EventDataValueTable field={view} data={data} />}
|
||||
{!event && <EventDataTable data={data} />}
|
||||
{event && <EventDataValueTable event={event} data={data} />}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import Link from 'next/link';
|
|||
import { useRouter } from 'next/router';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import ActiveUsers from 'components/metrics/ActiveUsers';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
import Icons from 'components/icons';
|
||||
import { useMessages, useWebsite } from 'hooks';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export function WebsiteHeader({ websiteId, showLinks = true, children }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -42,11 +42,11 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) {
|
|||
<Column className={styles.title} variant="two">
|
||||
<Favicon domain={domain} />
|
||||
<Text>{name}</Text>
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column className={styles.actions} variant="two">
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
{showLinks && (
|
||||
<Flexbox alignItems="center">
|
||||
<div className={styles.links}>
|
||||
{links.map(({ label, icon, path }) => {
|
||||
const selected = path ? pathname.endsWith(path) : pathname === '/websites/[id]';
|
||||
|
||||
|
|
@ -58,13 +58,13 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) {
|
|||
[styles.selected]: selected,
|
||||
})}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
<Icon className={styles.icon}>{icon}</Icon>
|
||||
<Text className={styles.label}>{label}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Flexbox>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -27,3 +27,29 @@
|
|||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.links {
|
||||
justify-content: space-evenly;
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.icon svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import WebsiteHeader from './WebsiteHeader';
|
|||
|
||||
export function WebsiteReportsPage({ websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { reports, error, isLoading } = useReports(websiteId);
|
||||
const { reports, error, isLoading, deleteReport } = useReports(websiteId);
|
||||
|
||||
const handleDelete = async id => {
|
||||
await deleteReport(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
|
|
@ -22,7 +26,7 @@ export function WebsiteReportsPage({ websiteId }) {
|
|||
</Button>
|
||||
</Link>
|
||||
</Flexbox>
|
||||
<ReportsTable websiteId={websiteId} data={reports} />
|
||||
<ReportsTable data={reports} onDelete={handleDelete} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue