Feat/um 145 re add events (#1733)

* Re-add events. Fix event_type queries.

* Re-add eventData

* revert CSS.
This commit is contained in:
Brian Cao 2023-01-05 19:39:29 -08:00 committed by GitHub
parent 796c65fa29
commit fcb1767eb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 222 additions and 167 deletions

View File

@ -24,7 +24,7 @@ function EventDataButton({ websiteId }) {
<Button
tooltip={<FormattedMessage id="label.event-data" defaultMessage="Event" />}
tooltipId="button-event"
size="small"
size="sm"
onClick={handleClick}
className={styles.button}
>
@ -34,8 +34,11 @@ function EventDataButton({ websiteId }) {
Event Data
</Button>
{showEventData && (
<Modal title={<FormattedMessage id="label.event-data" defaultMessage="Query Event Data" />}>
<EventDataForm websiteId={websiteId} onClose={handleClose} />
<Modal
title={<FormattedMessage id="label.event-data" defaultMessage="Query Event Data" />}
onClose={handleClose}
>
{close => <EventDataForm websiteId={websiteId} onClose={close} />}
</Modal>
)}
</>

View File

@ -1,3 +1,4 @@
.button {
width: fit-content;
margin-bottom: 15px;
}

View File

@ -1,22 +1,26 @@
import { useMutation } from '@tanstack/react-query';
import classNames from 'classnames';
import { Button } from 'react-basics';
import DateFilter from 'components/common/DateFilter';
import DropDown from 'components/common/DropDown';
import FormLayout, {
FormButtons,
FormError,
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import DataTable from 'components/metrics/DataTable';
import FilterTags from 'components/metrics/FilterTags';
import { Field, Form, Formik } from 'formik';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import { useState, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import styles from './EventDataForm.module.css';
import useTimezone from 'hooks/useTimezone';
import { useEffect, useState, useRef } from 'react';
import {
Button,
Dropdown,
Flexbox,
Form,
FormButtons,
FormInput,
FormRow,
Item,
TextField,
} from 'react-basics';
import { FormattedMessage } from 'react-intl';
import { FormMessage } from './../layout/FormLayout';
import styles from './EventDataForm.module.css';
export const filterOptions = [
{ label: 'Count', value: 'count' },
@ -77,13 +81,17 @@ export const dateOptions = [
export default function EventDataForm({ websiteId, onClose, className }) {
const { post } = useApi();
const [message, setMessage] = useState();
const { mutate } = useMutation(data => post(`/websites/${websiteId}/eventdata`, data));
const [columns, setColumns] = useState({});
const [filters, setFilters] = useState({});
const [type, setType] = useState('');
const [data, setData] = useState([]);
const [dateRange, setDateRange] = useDateRange('report');
const { startDate, endDate, value } = dateRange;
const [timezone] = useTimezone();
const [isValid, setIsValid] = useState(false);
const columnRef = useRef(null);
const filterRef = useRef(null);
useEffect(() => {
if (Object.keys(columns).length > 0) {
@ -93,9 +101,14 @@ export default function EventDataForm({ websiteId, onClose, className }) {
}
}, [columns]);
const handleAddTag = (value, list, setState, resetForm) => {
const handleAddTag = (value, list, setState, ref, clearDropdown) => {
setState({ ...list, [`${value.field}`]: value.value });
resetForm();
ref.current.reset({ field: '', value: '' });
if (clearDropdown) {
setType('');
}
};
const handleRemoveTag = (value, list, setState) => {
@ -116,134 +129,102 @@ export default function EventDataForm({ websiteId, onClose, className }) {
filters,
};
const { ok, data } = await post(`/websites/${websiteId}/eventdata`, params);
if (!ok) {
setMessage(<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />);
setData([]);
} else {
setData(data);
setMessage(null);
}
mutate(params, {
onSuccess: async data => {
setData(data);
setMessage(null);
},
onError: async () => {
setMessage(
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
);
setData([]);
},
});
};
return (
<>
<FormMessage>{message}</FormMessage>
<Flexbox className={styles.message} alignItems="center" direction="column">
<FormMessage>{message}</FormMessage>
</Flexbox>
<div className={classNames(styles.container, className)}>
<div className={styles.form}>
<FormLayout>
<div className={styles.filters}>
<FormRow>
<label htmlFor="date-range">
<FormattedMessage id="label.date-range" defaultMessage="Date Range" />
</label>
<DateFilter
value={value}
startDate={startDate}
endDate={endDate}
onChange={setDateRange}
options={dateOptions}
/>
<div className={styles.filters}>
<FormRow label={<FormattedMessage id="label.date-range" defaultMessage="Date Range" />}>
<DateFilter
value={value}
startDate={startDate}
endDate={endDate}
onChange={setDateRange}
options={dateOptions}
/>
</FormRow>
</div>
<div className={styles.filters}>
<Form
ref={columnRef}
onSubmit={value =>
handleAddTag({ ...value, value: type }, columns, setColumns, columnRef, true)
}
>
<FormInput
name="field"
label={<FormattedMessage id="label.field-name" defaultMessage="Field Name" />}
rules={{ required: 'Required' }}
>
<TextField />
</FormInput>
<FormRow
name="value"
label={<FormattedMessage id="label.type" defaultMessage="Type" />}
>
<Dropdown items={filterOptions} value={type} onChange={setType}>
{({ value, label }) => <Item key={value}>{label}</Item>}
</Dropdown>
</FormRow>
</div>
<div className={styles.filters}>
<Formik
initialValues={{ field: '', value: '' }}
onSubmit={(value, { resetForm }) =>
handleAddTag(value, columns, setColumns, resetForm)
}
<FormButtons className={styles.formButtons}>
<Button variant="action" type="submit">
<FormattedMessage id="label.add-column" defaultMessage="Add Column" />
</Button>
</FormButtons>
</Form>
<FilterTags
className={styles.filterTag}
params={columns}
onClick={value => handleRemoveTag(value, columns, setColumns)}
/>
</div>
<div className={styles.filters}>
<Form
ref={filterRef}
onSubmit={value => handleAddTag(value, filters, setFilters, filterRef)}
>
<FormInput
name="field"
label={<FormattedMessage id="label.field-name" defaultMessage="Field Name" />}
>
{({ values, setFieldValue }) => (
<Form>
<FormRow>
<label htmlFor="field">
<FormattedMessage id="label.field-name" defaultMessage="Field Name" />
</label>
<div>
<Field name="field" type="text" />
<FormError name="field" />
</div>
</FormRow>
<FormRow>
<label htmlFor="value">
<FormattedMessage id="label.type" defaultMessage="Type" />
</label>
<div>
<DropDown
value={values.value}
onChange={value => setFieldValue('value', value)}
className={styles.dropdown}
name="value"
options={filterOptions}
/>
<FormError name="value" />
</div>
</FormRow>
<FormButtons className={styles.formButtons}>
<Button
variant="action"
type="submit"
disabled={!values.field || !values.value}
>
<FormattedMessage id="label.add-column" defaultMessage="Add Column" />
</Button>
</FormButtons>
</Form>
)}
</Formik>
<FilterTags
className={styles.filterTag}
params={columns}
onClick={value => handleRemoveTag(value, columns, setColumns)}
/>
</div>
<div className={styles.filters}>
<Formik
initialValues={{ field: '', value: '' }}
onSubmit={(value, { resetForm }) =>
handleAddTag(value, filters, setFilters, resetForm)
}
<TextField />
</FormInput>
<FormInput
name="value"
label={<FormattedMessage id="label.value" defaultMessage="Value" />}
>
{({ values }) => (
<Form>
<FormRow>
<label htmlFor="field">
<FormattedMessage id="label.field-name" defaultMessage="Field Name" />
</label>
<div>
<Field name="field" type="text" />
<FormError name="field" />
</div>
</FormRow>
<FormRow>
<label htmlFor="value">
<FormattedMessage id="label.value" defaultMessage="Value" />
</label>
<div>
<Field name="value" type="text" />
<FormError name="value" />
</div>
</FormRow>
<FormButtons className={styles.formButtons}>
<Button
variant="action"
type="submit"
disabled={!values.field || !values.value}
>
<FormattedMessage id="label.add-filter" defaultMessage="Add Filter" />
</Button>
</FormButtons>
</Form>
)}
</Formik>
<FilterTags
className={styles.filterTag}
params={filters}
onClick={value => handleRemoveTag(value, filters, setFilters)}
/>
</div>
</FormLayout>
<TextField />
</FormInput>
<FormButtons className={styles.formButtons}>
<Button variant="action" type="submit">
<FormattedMessage id="label.add-filter" defaultMessage="Add Filter" />
</Button>
</FormButtons>
</Form>
<FilterTags
className={styles.filterTag}
params={filters}
onClick={value => handleRemoveTag(value, filters, setFilters)}
/>
</div>
</div>
<div>
<DataTable className={styles.table} data={data} title="Results" showPercentage={false} />

View File

@ -3,8 +3,9 @@
}
.form {
padding: 20px;
border-right: 1px solid var(--base300);
width: 420px;
width: 425px;
}
.filters {
@ -16,6 +17,10 @@
min-height: 250px;
}
.message {
width: 100%;
}
.table {
padding: 10px;
min-height: 430px;

View File

@ -1,6 +1,6 @@
import classNames from 'classnames';
import { safeDecodeURI } from 'next-basics';
import { Button } from 'react-basics';
import { Button, Icon } from 'react-basics';
import Times from 'assets/times.svg';
import styles from './FilterTags.module.css';
@ -16,8 +16,11 @@ export default function FilterTags({ className, params, onClick }) {
}
return (
<div key={key} className={styles.tag}>
<Button icon={<Times />} onClick={() => onClick(key)} variant="action" iconRight>
<Button onClick={() => onClick(key)} variant="action" iconRight>
{`${key}: ${safeDecodeURI(params[key])}`}
<Icon>
<Times />
</Icon>
</Button>
</div>
);

View File

@ -1,26 +1,29 @@
import { useState } from 'react';
import { Column, Loading } from 'react-basics';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Arrow from 'assets/arrow-right.svg';
import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart';
import Link from 'components/common/Link';
import WorldMap from 'components/common/WorldMap';
import Page from 'components/layout/Page';
import GridRow from 'components/layout/GridRow';
import MenuLayout from 'components/layout/MenuLayout';
import Link from 'components/common/Link';
import PagesTable from 'components/metrics/PagesTable';
import ReferrersTable from 'components/metrics/ReferrersTable';
import Page from 'components/layout/Page';
import BrowsersTable from 'components/metrics/BrowsersTable';
import OSTable from 'components/metrics/OSTable';
import DevicesTable from 'components/metrics/DevicesTable';
import CountriesTable from 'components/metrics/CountriesTable';
import DevicesTable from 'components/metrics/DevicesTable';
import LanguagesTable from 'components/metrics/LanguagesTable';
import ScreenTable from 'components/metrics/ScreenTable';
import OSTable from 'components/metrics/OSTable';
import PagesTable from 'components/metrics/PagesTable';
import QueryParametersTable from 'components/metrics/QueryParametersTable';
import usePageQuery from 'hooks/usePageQuery';
import ReferrersTable from 'components/metrics/ReferrersTable';
import ScreenTable from 'components/metrics/ScreenTable';
import WebsiteChart from 'components/metrics/WebsiteChart';
import useApi from 'hooks/useApi';
import usePageQuery from 'hooks/usePageQuery';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import Arrow from 'assets/arrow-right.svg';
import { useState } from 'react';
import { Column, Loading } from 'react-basics';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import EventDataButton from './../common/EventDataButton';
import EventsChart from './../metrics/EventsChart';
import EventsTable from './../metrics/EventsTable';
import styles from './WebsiteDetails.module.css';
const messages = defineMessages({
@ -32,6 +35,7 @@ const messages = defineMessages({
devices: { id: 'metrics.devices', defaultMessage: 'Devices' },
countries: { id: 'metrics.countries', defaultMessage: 'Countries' },
languages: { id: 'metrics.languages', defaultMessage: 'Languages' },
events: { id: 'metrics.events', defaultMessage: 'Events' },
query: { id: 'metrics.query-parameters', defaultMessage: 'Query parameters' },
});
@ -44,6 +48,7 @@ const views = {
screen: ScreenTable,
country: CountriesTable,
language: LanguagesTable,
event: EventsTable,
query: QueryParametersTable,
};
@ -104,6 +109,10 @@ export default function WebsiteDetails({ websiteId }) {
label: formatMessage(messages.screens),
value: resolve({ view: 'screen' }),
},
{
label: formatMessage(messages.events),
value: resolve({ view: 'event' }),
},
{
label: formatMessage(messages.query),
value: resolve({ view: 'query' }),
@ -172,6 +181,15 @@ export default function WebsiteDetails({ websiteId }) {
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
</Column>
</GridRow>
<GridRow>
<Column xs={12} md={12} lg={4} defaultSize={4}>
<EventsTable {...tableProps} />
</Column>
<Column xs={12} md={12} lg={8} defaultSize={8}>
<EventDataButton websiteId={websiteId} />
<EventsChart className={styles.eventschart} websiteId={websiteId} />
</Column>
</GridRow>
</>
)}
{view && chartLoaded && (

View File

@ -94,7 +94,7 @@
"npm-run-all": "^4.1.5",
"prop-types": "^15.7.2",
"react": "^18.2.0",
"react-basics": "^0.44.0",
"react-basics": "^0.48.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-intl": "^5.24.7",
@ -109,6 +109,7 @@
"thenby": "^1.3.4",
"timezone-support": "^2.0.2",
"uuid": "^8.3.2",
"yup": "^0.32.11",
"zustand": "^3.7.2"
},
"devDependencies": {

View File

@ -139,8 +139,8 @@ export default async (
startDate,
endDate,
column,
table,
filters,
type,
});
return ok(res, data);

View File

@ -12,8 +12,8 @@ export async function getPageviewMetrics(
startDate: Date;
endDate: Date;
column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum;
table: string;
filters: object;
type: string;
},
]
) {
@ -30,11 +30,16 @@ async function relationalQuery(
endDate: Date;
column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum;
filters: object;
type: string;
},
) {
const { startDate, endDate, column, filters = {} } = data;
const { startDate, endDate, column, filters = {}, type } = data;
const { rawQuery, parseFilters } = prisma;
const params = [startDate, endDate];
const params: any = [
startDate,
endDate,
type === 'event' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
];
const { filterQuery, joinSession } = parseFilters(filters, params);
return rawQuery(
@ -43,7 +48,7 @@ async function relationalQuery(
${joinSession}
where website_id='${websiteId}'
and website_event.created_at between $1 and $2
and event_type = ${EVENT_TYPE.pageView}
and event_type = $3
${filterQuery}
group by 1
order by 2 desc`,
@ -58,12 +63,17 @@ async function clickhouseQuery(
endDate: Date;
column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum;
filters: object;
type: string;
},
) {
const { startDate, endDate, column, filters = {} } = data;
const { startDate, endDate, column, filters = {}, type } = data;
const { rawQuery, parseFilters, getBetweenDates } = clickhouse;
const website = await cache.fetchWebsite(websiteId);
const params = [websiteId, website?.revId || 0, EVENT_TYPE.pageView];
const params = [
websiteId,
website?.revId || 0,
type === 'event' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
];
const { filterQuery } = parseFilters(filters, params);
return rawQuery(

View File

@ -32,7 +32,7 @@ async function clickhouseQuery(websites, startAt) {
created_at,
url
from event
where event_name = ''
where event_type = 1
and ${
websites && websites.length > 0
? `website_id in (${getCommaSeparatedStringFormat(websites)})`

View File

@ -58,7 +58,7 @@ async function clickhouseQuery(
from event as x
where website_id = $1
and rev_id = $2
and event_name = ''
and event_type = 1
and ${getBetweenDates('created_at', startDate, endDate)}
${filterQuery}
group by x

View File

@ -67,7 +67,7 @@ async function clickhouseQuery(
min(created_at) min_time,
max(created_at) max_time
from event
where event_name = ''
where event_type = 1
and website_id = $1
and rev_id = $2
and ${getBetweenDates('created_at', startDate, endDate)}

View File

@ -2176,6 +2176,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash@^4.14.175":
version "4.14.191"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
"@types/minimist@^1.2.0":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
@ -5545,6 +5550,11 @@ murmurhash@^2.0.0:
resolved "https://registry.yarnpkg.com/murmurhash/-/murmurhash-2.0.1.tgz#4097720e08cf978872194ad84ea5be2dec9b610f"
integrity sha512-5vQEh3y+DG/lMPM0mCGPDnyV8chYg/g7rl6v3Gd8WMF9S429ox3Xk8qrk174kWhG767KQMqqxLD1WnGd77hiew==
nanoclone@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
@ -6353,6 +6363,11 @@ prop-types@^15.7.2, prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
property-expr@^2.0.4:
version "2.0.5"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
psl@^1.1.24:
version "1.9.0"
resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz"
@ -6423,10 +6438,10 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-basics@^0.44.0:
version "0.44.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.44.0.tgz#5c022da288d8a3da6b32111704acf1113e04b15e"
integrity sha512-O8W/zPCRHDGKfyGDLBZBMXE9Q2EkK+hUD+bdkUNMLQsDq1ON15uOKwlcUff2lIjwi1LxHuq7Dbwrdn6yxXH4JQ==
react-basics@^0.48.0:
version "0.48.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.48.0.tgz#f86924b6e0fbe67e55e3c27763390f4d3796edb4"
integrity sha512-0pbPsylQevm08VGY7aqqToLh9Xl764FNDPp6NxnwHlJSL9jvYRxhW2ajivsZOfeIPJgRiWbmmEBGmTmSwwhiIQ==
dependencies:
classnames "^2.3.1"
react "^18.2.0"
@ -7572,6 +7587,11 @@ topojson-client@^3.1.0:
dependencies:
commander "2"
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz"
@ -8013,6 +8033,19 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
yup@^0.32.11:
version "0.32.11"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5"
integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==
dependencies:
"@babel/runtime" "^7.15.4"
"@types/lodash" "^4.14.175"
lodash "^4.17.21"
lodash-es "^4.17.21"
nanoclone "^0.2.1"
property-expr "^2.0.4"
toposort "^2.0.2"
zustand@^3.7.2:
version "3.7.2"
resolved "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz"