Redo events tab to show all events.

This commit is contained in:
Mike Cao 2024-08-01 21:05:43 -07:00
parent 10f65cae68
commit 9c32057841
18 changed files with 152 additions and 66 deletions

View File

@ -48,7 +48,7 @@ export function EventDataParameters() {
groups, groups,
}; };
const handleSubmit = values => { const handleSubmit = (values: any) => {
runReport(values); runReport(values);
}; };

View File

@ -1,12 +0,0 @@
'use client';
import WebsiteHeader from '../WebsiteHeader';
import WebsiteEventData from './WebsiteEventData';
export default function EventDataPage({ websiteId }) {
return (
<>
<WebsiteHeader websiteId={websiteId} />
<WebsiteEventData websiteId={websiteId} />
</>
);
}

View File

@ -1,37 +0,0 @@
import Link from 'next/link';
import { GridTable, GridColumn } from 'react-basics';
import { useMessages, useNavigation } from 'components/hooks';
import Empty from 'components/common/Empty';
import { DATA_TYPES } from 'lib/constants';
export function EventDataTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
const { renderUrl } = useNavigation();
if (data.length === 0) {
return <Empty />;
}
return (
<GridTable data={data}>
<GridColumn name="eventName" label={formatMessage(labels.event)}>
{row => (
<Link href={renderUrl({ event: row.eventName })} shallow={true}>
{row.eventName}
</Link>
)}
</GridColumn>
<GridColumn name="fieldName" label={formatMessage(labels.field)}>
{row => row.fieldName}
</GridColumn>
<GridColumn name="dataType" label={formatMessage(labels.type)}>
{row => DATA_TYPES[row.dataType]}
</GridColumn>
<GridColumn name="total" label={formatMessage(labels.totalRecords)}>
{({ total }) => total.toLocaleString()}
</GridColumn>
</GridTable>
);
}
export default EventDataTable;

View File

@ -0,0 +1,25 @@
import { useWebsiteEvents } from 'components/hooks';
import EventsTable from './EventsTable';
import DataTable from 'components/common/DataTable';
import { ReactNode } from 'react';
export default function EventsDataTable({
websiteId,
children,
}: {
websiteId?: string;
teamId?: string;
children?: ReactNode;
}) {
const queryResult = useWebsiteEvents(websiteId);
if (queryResult?.result?.data?.length === 0) {
return children;
}
return (
<DataTable queryResult={queryResult} allowSearch={false}>
{({ data }) => <EventsTable data={data} />}
</DataTable>
);
}

View File

@ -0,0 +1,12 @@
'use client';
import WebsiteHeader from '../WebsiteHeader';
import EventsDataTable from './EventsDataTable';
export default function EventsPage({ websiteId }) {
return (
<>
<WebsiteHeader websiteId={websiteId} />
<EventsDataTable websiteId={websiteId} />
</>
);
}

View File

@ -0,0 +1,43 @@
import { GridTable, GridColumn } from 'react-basics';
import { useLocale, useMessages } from 'components/hooks';
import Empty from 'components/common/Empty';
import { formatDistance } from 'date-fns';
import Profile from 'components/common/Profile';
import Link from 'next/link';
export function EventsTable({ data = [] }) {
const { dateLocale } = useLocale();
const { formatMessage, labels } = useMessages();
if (data.length === 0) {
return <Empty />;
}
return (
<GridTable data={data}>
<GridColumn name="id" label="ID" />
<GridColumn name="session" label={formatMessage(labels.session)}>
{row => (
<Link href={`/sessions/`}>
<Profile seed={row.sessionId} size={64} />
</Link>
)}
</GridColumn>
<GridColumn name="eventName" label={formatMessage(labels.event)}>
{row => formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
</GridColumn>
<GridColumn name="eventName" label={formatMessage(labels.name)} />
<GridColumn name="urlPath" label={formatMessage(labels.path)} />
<GridColumn name="created" label={formatMessage(labels.created)}>
{row =>
formatDistance(new Date(row.createdAt), new Date(), {
addSuffix: true,
locale: dateLocale,
})
}
</GridColumn>
</GridTable>
);
}
export default EventsTable;

View File

@ -1,5 +1,5 @@
import { Flexbox, Loading } from 'react-basics'; import { Flexbox, Loading } from 'react-basics';
import EventDataTable from './EventDataTable'; import EventsTable from './EventsTable';
import EventDataValueTable from './EventDataValueTable'; import EventDataValueTable from './EventDataValueTable';
import { EventDataMetricsBar } from './EventDataMetricsBar'; import { EventDataMetricsBar } from './EventDataMetricsBar';
import { useDateRange, useApi, useNavigation } from 'components/hooks'; import { useDateRange, useApi, useNavigation } from 'components/hooks';
@ -33,7 +33,7 @@ export default function WebsiteEventData({ websiteId }) {
return ( return (
<Flexbox className={styles.container} direction="column" gap={20}> <Flexbox className={styles.container} direction="column" gap={20}>
<EventDataMetricsBar websiteId={websiteId} /> <EventDataMetricsBar websiteId={websiteId} />
{!event && <EventDataTable data={data} />} {!event && <EventsTable data={data} />}
{isLoading && <Loading position="page" />} {isLoading && <Loading position="page" />}
{event && data && <EventDataValueTable event={event} data={data} />} {event && data && <EventDataValueTable event={event} data={data} />}
</Flexbox> </Flexbox>

View File

@ -1,8 +1,8 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import EventDataPage from './EventDataPage'; import EventsPage from './EventsPage';
export default async function ({ params: { websiteId } }) { export default async function ({ params: { websiteId } }) {
return <EventDataPage websiteId={websiteId} />; return <EventsPage websiteId={websiteId} />;
} }
export const metadata: Metadata = { export const metadata: Metadata = {

View File

@ -1,7 +1,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Banner, Loading, SearchField } from 'react-basics'; import { Banner, Loading, SearchField } from 'react-basics';
import { useMessages } from 'components/hooks'; import { useMessages, useNavigation } from 'components/hooks';
import Empty from 'components/common/Empty'; import Empty from 'components/common/Empty';
import Pager from 'components/common/Pager'; import Pager from 'components/common/Pager';
import { FilterQueryResult } from 'lib/types'; import { FilterQueryResult } from 'lib/types';
@ -35,6 +35,7 @@ export function DataTable({
const { query } = params || {}; const { query } = params || {};
const hasData = Boolean(!isLoading && data?.length); const hasData = Boolean(!isLoading && data?.length);
const noResults = Boolean(!isLoading && query && !hasData); const noResults = Boolean(!isLoading && query && !hasData);
const { router, renderUrl } = useNavigation();
const handleSearch = (query: string) => { const handleSearch = (query: string) => {
setParams({ ...params, query, page: params.page ? page : 1 }); setParams({ ...params, query, page: params.page ? page : 1 });
@ -42,6 +43,7 @@ export function DataTable({
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
setParams({ ...params, query, page }); setParams({ ...params, query, page });
router.push(renderUrl({ page }));
}; };
if (error) { if (error) {

View File

@ -2,15 +2,17 @@ import { UseQueryOptions } from '@tanstack/react-query';
import { useState } from 'react'; import { useState } from 'react';
import { useApi } from './useApi'; import { useApi } from './useApi';
import { PageResult, PageParams, FilterQueryResult } from 'lib/types'; import { PageResult, PageParams, FilterQueryResult } from 'lib/types';
import { useNavigation } from '../useNavigation';
export function useFilterQuery<T = any>({ export function useFilterQuery<T = any>({
queryKey, queryKey,
queryFn, queryFn,
...options ...options
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): FilterQueryResult<T> { }: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): FilterQueryResult<T> {
const [params, setParams] = useState<T | PageParams>({ const { query: queryParams } = useNavigation();
const [params, setParams] = useState<PageParams>({
query: '', query: '',
page: 1, page: +queryParams.page || 1,
}); });
const { useQuery } = useApi(); const { useQuery } = useApi();
@ -21,7 +23,7 @@ export function useFilterQuery<T = any>({
}); });
return { return {
result: data as PageResult<any>, result: data as PageResult<T>,
query, query,
params, params,
setParams, setParams,

View File

@ -1,17 +1,19 @@
import useApi from './useApi'; import useApi from './useApi';
import { useFilterParams } from '../useFilterParams';
import { UseQueryOptions } from '@tanstack/react-query'; import { UseQueryOptions } from '@tanstack/react-query';
import { useFilterParams } from '../useFilterParams';
import { useFilterQuery } from 'components/hooks';
export function useWebsiteEvents( export function useWebsiteEvents(
websiteId: string, websiteId: string,
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>, options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
) { ) {
const { get, useQuery } = useApi(); const { get } = useApi();
const params = useFilterParams(websiteId); const params = useFilterParams(websiteId);
return useQuery({ return useFilterQuery({
queryKey: ['websites:events', { websiteId, ...params }], queryKey: ['websites:events', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/events`, params), queryFn: pageParams =>
get(`/websites/${websiteId}/events`, { ...params, ...pageParams, pageSize: 20 }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View File

@ -130,6 +130,7 @@ export const labels = defineMessages({
selectRole: { id: 'label.select-role', defaultMessage: 'Select role' }, selectRole: { id: 'label.select-role', defaultMessage: 'Select role' },
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' }, selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
all: { id: 'label.all', defaultMessage: 'All' }, all: { id: 'label.all', defaultMessage: 'All' },
session: { id: 'label.session', defaultMessage: 'Session' },
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' }, pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' },
activityLog: { id: 'label.activity-log', defaultMessage: 'Activity log' }, activityLog: { id: 'label.activity-log', defaultMessage: 'Activity log' },
@ -275,6 +276,7 @@ export const labels = defineMessages({
lastSeen: { id: 'label.last-seen', defaultMessage: 'Last seen' }, lastSeen: { id: 'label.last-seen', defaultMessage: 'Last seen' },
firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' }, firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' },
properties: { id: 'label.properties', defaultMessage: 'Properties' }, properties: { id: 'label.properties', defaultMessage: 'Properties' },
path: { id: 'label.path', defaultMessage: 'Path' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({

View File

@ -0,0 +1,42 @@
import * as yup from 'yup';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { pageInfo } from 'lib/schema';
import { getWebsiteEvents } from 'queries';
export interface ReportsRequestQuery extends PageParams {
websiteId: string;
}
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
...pageInfo,
}),
};
export default async (
req: NextApiRequestQueryBody<ReportsRequestQuery, any>,
res: NextApiResponse,
) => {
await useCors(req, res);
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
return unauthorized(res);
}
const data = await getWebsiteEvents(websiteId, {}, req.query);
return ok(res, data);
}
return methodNotAllowed(res);
};

View File

@ -3,7 +3,7 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { PageParams, QueryFilters } from 'lib/types'; import { PageParams, QueryFilters } from 'lib/types';
export function getEvents( export function getWebsiteEvents(
...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams] ...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams]
) { ) {
return runQuery({ return runQuery({

View File

@ -1,4 +1,9 @@
import { getWebsiteSessions, getEvents, getPageviewStats, getSessionStats } from 'queries/index'; import {
getWebsiteSessions,
getWebsiteEvents,
getPageviewStats,
getSessionStats,
} from 'queries/index';
const MAX_SIZE = 50; const MAX_SIZE = 50;
@ -19,7 +24,7 @@ export async function getRealtimeData(
const { startDate, timezone } = criteria; const { startDate, timezone } = criteria;
const filters = { startDate, endDate: new Date(), unit: 'minute', timezone }; const filters = { startDate, endDate: new Date(), unit: 'minute', timezone };
const [events, sessions, pageviews, sessionviews] = await Promise.all([ const [events, sessions, pageviews, sessionviews] = await Promise.all([
getEvents(websiteId, { startDate, timezone }, { pageSize: 10000 }), getWebsiteEvents(websiteId, { startDate, timezone }, { pageSize: 10000 }),
getWebsiteSessions(websiteId, { startDate, timezone }, { pageSize: 10000 }), getWebsiteSessions(websiteId, { startDate, timezone }, { pageSize: 10000 }),
getPageviewStats(websiteId, filters), getPageviewStats(websiteId, filters),
getSessionStats(websiteId, filters), getSessionStats(websiteId, filters),

View File

@ -8,7 +8,7 @@ export * from './analytics/events/getEventDataFields';
export * from './analytics/events/getEventDataStats'; export * from './analytics/events/getEventDataStats';
export * from './analytics/events/getEventDataUsage'; export * from './analytics/events/getEventDataUsage';
export * from './analytics/events/getEventMetrics'; export * from './analytics/events/getEventMetrics';
export * from './analytics/events/getEvents'; export * from './analytics/events/getWebsiteEvents';
export * from './analytics/events/getEventUsage'; export * from './analytics/events/getEventUsage';
export * from './analytics/events/saveEvent'; export * from './analytics/events/saveEvent';
export * from './analytics/reports/getFunnel'; export * from './analytics/reports/getFunnel';