Merge remote-tracking branch 'upstream/master'

This commit is contained in:
treturner 2023-08-03 09:46:30 -04:00
commit a20e3483e0
242 changed files with 18823 additions and 3413 deletions

View 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;

View file

@ -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>

View 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>
);
}

View 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);
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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 (

View file

@ -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>

View file

@ -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;
}

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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">

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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!',
},
});

View file

@ -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>
);

View file

@ -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 {

View file

@ -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}

View file

@ -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();

View file

@ -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>
);
}

View file

@ -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()}

View file

@ -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;

View file

@ -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 (

View file

@ -0,0 +1,5 @@
.row {
display: flex;
align-items: center;
gap: 10px;
}

View file

@ -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];

View file

@ -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} />

View file

@ -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 }) {

View file

@ -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];

View file

@ -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>
);

View file

@ -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}>

View file

@ -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)} />

View file

@ -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>
)}
</>
);
}

View file

@ -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) => {

View file

@ -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}
/>
);
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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)}

View file

@ -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],
);

View file

@ -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;

View file

@ -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;
})}

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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>
);
}