diff --git a/components/input/LanguageButton.module.css b/components/input/LanguageButton.module.css index 16d61978..e46729c0 100644 --- a/components/input/LanguageButton.module.css +++ b/components/input/LanguageButton.module.css @@ -1,8 +1,7 @@ .menu { display: flex; flex-flow: row wrap; - min-width: 600px; - max-width: 100vw; + max-width: 640px; padding: 10px; background: var(--base50); z-index: var(--z-index-popup); diff --git a/components/messages.js b/components/messages.js index adae6aa2..bcb2a5c1 100644 --- a/components/messages.js +++ b/components/messages.js @@ -129,6 +129,8 @@ export const labels = defineMessages({ window: { id: 'label.window', defaultMessage: 'Window' }, addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' }, runQuery: { id: 'label.run-query', defaultMessage: 'Run query' }, + fields: { id: 'label.fields', defaultMessage: 'Fields' }, + addField: { id: 'label.add-field', defaultMessage: 'Add field' }, }); export const messages = defineMessages({ diff --git a/components/pages/reports/BaseParameters.js b/components/pages/reports/BaseParameters.js new file mode 100644 index 00000000..2d04f228 --- /dev/null +++ b/components/pages/reports/BaseParameters.js @@ -0,0 +1,43 @@ +import { FormRow } from 'react-basics'; +import DateFilter from 'components/input/DateFilter'; +import WebsiteSelect from 'components/input/WebsiteSelect'; +import { parseDateRange } from 'lib/date'; +import { useContext } from 'react'; +import { ReportContext } from './Report'; +import { useMessages } from 'hooks'; + +export function BaseParameters() { + const { report, updateReport } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + + const { parameters } = report || {}; + const { websiteId, dateRange } = parameters || {}; + const { value, startDate, endDate } = dateRange || {}; + + const handleWebsiteSelect = websiteId => { + updateReport({ parameters: { websiteId } }); + }; + + const handleDateChange = value => { + updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } }); + }; + + return ( + <> + + + + + + + + ); +} + +export default BaseParameters; diff --git a/components/pages/reports/Report.js b/components/pages/reports/Report.js index 0239e64f..0a98ef75 100644 --- a/components/pages/reports/Report.js +++ b/components/pages/reports/Report.js @@ -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 ( diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js index 42d78e75..08465ef9 100644 --- a/components/pages/reports/ReportHeader.js +++ b/components/pages/reports/ReportHeader.js @@ -64,24 +64,14 @@ export function ReportHeader({ icon }) { return ( } className={styles.header}> - - - - - {formatMessage(labels.save)} - - + + {formatMessage(labels.save)} + {toast} ); diff --git a/components/pages/reports/ReportList.js b/components/pages/reports/ReportList.js index da0a622c..6e07c81e 100644 --- a/components/pages/reports/ReportList.js +++ b/components/pages/reports/ReportList.js @@ -10,7 +10,7 @@ import styles from './ReportList.module.css'; const reports = [ { title: 'Event data', - description: 'Query your event data.', + description: 'Query your custom event data.', url: '/reports/event-data', icon: , }, diff --git a/components/pages/reports/event-data/EventDataParameters.js b/components/pages/reports/event-data/EventDataParameters.js new file mode 100644 index 00000000..cbb14193 --- /dev/null +++ b/components/pages/reports/event-data/EventDataParameters.js @@ -0,0 +1,66 @@ +import { useContext, useRef } from 'react'; +import { useApi, useMessages } from 'hooks'; +import { Form, FormRow, FormButtons, SubmitButton, Loading } from 'react-basics'; +import { ReportContext } from 'components/pages/reports/Report'; +import NoData from 'components/common/NoData'; +import styles from './EventDataParameters.module.css'; +import { DATA_TYPES } from 'lib/constants'; + +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 EventDataParameters() { + const { report, runReport, isRunning } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + const ref = useRef(null); + const { parameters } = report || {}; + const { websiteId, dateRange } = parameters || {}; + const { startDate, endDate } = dateRange || {}; + const queryDisabled = !websiteId || !dateRange; + const { data, error, isLoading } = useFields(websiteId, startDate, endDate); + + const handleSubmit = values => { + runReport(values); + }; + + if (!websiteId || !dateRange) { + return null; + } + + if (isLoading) { + return ; + } + + return ( +
+ +
+ {!data?.length && } + {data?.map?.(({ eventKey, eventDataType }) => { + return ( +
+
{eventKey}
+
{DATA_TYPES[eventDataType]}
+
+ ); + })} +
+
+ + + {formatMessage(labels.runQuery)} + + +
+ ); +} + +export default EventDataParameters; diff --git a/components/pages/reports/event-data/EventDataParameters.module.css b/components/pages/reports/event-data/EventDataParameters.module.css new file mode 100644 index 00000000..66c82842 --- /dev/null +++ b/components/pages/reports/event-data/EventDataParameters.module.css @@ -0,0 +1,27 @@ +.fields { + max-height: 300px; + overflow: auto; +} + +.field { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + height: 30px; + cursor: pointer; + padding: 4px; + border-radius: var(--border-radius); +} + +.field:hover { + background: var(--base75); +} + +.key { + font-weight: 400; +} + +.type { + color: var(--font-color300); +} diff --git a/components/pages/reports/event-data/EventDataReport.js b/components/pages/reports/event-data/EventDataReport.js index a32cd6e2..16382982 100644 --- a/components/pages/reports/event-data/EventDataReport.js +++ b/components/pages/reports/event-data/EventDataReport.js @@ -1,30 +1,23 @@ -import { useState } from 'react'; -import { Form, FormRow, FormInput, TextField } from 'react-basics'; import Report from '../Report'; import ReportHeader from '../ReportHeader'; -import useMessages from 'hooks/useMessages'; +import ReportMenu from '../ReportMenu'; +import ReportBody from '../ReportBody'; +import EventDataParameters from './EventDataParameters'; import Nodes from 'assets/nodes.svg'; -import styles from '../reports.module.css'; -export default function EventDataReport({ websiteId, data }) { - const [values, setValues] = useState({ query: '' }); - const { formatMessage, labels } = useMessages(); +const defaultParameters = { + type: 'event-data', + parameters: { fields: [], filters: [] }, +}; +export default function EventDataReport({ reportId }) { return ( - - } /> -
-
-
- - - - - -
-
-
-
+ + } /> + + + + hi. ); } diff --git a/components/pages/reports/event-data/FieldAddForm.js b/components/pages/reports/event-data/FieldAddForm.js new file mode 100644 index 00000000..b0258608 --- /dev/null +++ b/components/pages/reports/event-data/FieldAddForm.js @@ -0,0 +1,28 @@ +import { useMessages } from 'hooks'; +import { Button, Form, FormButtons, FormRow } from 'react-basics'; + +export function FieldAddForm({ onClose }) { + const { formatMessage, labels } = useMessages(); + + const handleSave = () => { + onClose(); + }; + + const handleClose = () => { + onClose(); + }; + + return ( +
+ + + + + +
+ ); +} + +export default FieldAddForm; diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js index c87d52e4..991bf690 100644 --- a/components/pages/reports/funnel/FunnelParameters.js +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -6,98 +6,99 @@ import { FormButtons, FormInput, FormRow, - Modal, + PopupTrigger, + Popup, SubmitButton, Text, TextField, Tooltip, } from 'react-basics'; import Icons from 'components/icons'; -import AddUrlForm from './AddUrlForm'; +import UrlAddForm from './UrlAddForm'; import { ReportContext } from 'components/pages/reports/Report'; import styles from './FunnelParameters.module.css'; +import BaseParameters from '../BaseParameters'; export function FunnelParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const [show, setShow] = useState(false); const ref = useRef(null); - const { websiteId, parameters } = report || {}; - const queryDisabled = !websiteId || parameters?.urls?.length < 2; + + const { parameters } = report || {}; + const { websiteId, dateRange, urls } = parameters || {}; + const queryDisabled = !websiteId || !dateRange || urls?.length < 2; const handleSubmit = values => { runReport(values); }; const handleAddUrl = url => { - updateReport({ parameters: { ...parameters, urls: parameters.urls.concat(url) } }); + updateReport({ parameters: { urls: parameters.urls.concat(url) } }); }; const handleRemoveUrl = (index, e) => { e.stopPropagation(); const urls = [...parameters.urls]; urls.splice(index, 1); - updateReport({ parameters: { ...parameters, urls } }); + updateReport({ parameters: { urls } }); }; - const showAddForm = () => setShow(true); - const hideAddForm = () => setShow(false); - return ( - <> -
- - - - - - }> -
- {parameters?.urls?.map((url, index) => { - return ( -
- {url} - - - - - -
- ); - })} -
-
- - - {formatMessage(labels.runQuery)} - - -
- {show && ( - - - - )} - +
+ + + + + + + }> +
+ {parameters?.urls?.map((url, index) => { + return ( +
+ {url} + + + + + +
+ ); + })} +
+
+ + + {formatMessage(labels.runQuery)} + + + ); } -function AddUrlButton({ onClick }) { +function AddUrlButton({ onAdd }) { const { formatMessage, labels } = useMessages(); return ( - - - - - + + + + + + + + {close => { + return ; + }} + + ); } diff --git a/components/pages/reports/funnel/FunnelReport.js b/components/pages/reports/funnel/FunnelReport.js index 26a81a09..7b4d8ece 100644 --- a/components/pages/reports/funnel/FunnelReport.js +++ b/components/pages/reports/funnel/FunnelReport.js @@ -1,17 +1,15 @@ -import { useContext } from 'react'; import FunnelChart from './FunnelChart'; import FunnelTable from './FunnelTable'; import FunnelParameters from './FunnelParameters'; -import Report, { ReportContext } from '../Report'; +import Report from '../Report'; import ReportHeader from '../ReportHeader'; import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; import Funnel from 'assets/funnel.svg'; -import { useReport } from 'hooks'; const defaultParameters = { type: 'funnel', - parameters: { window: 60, urls: ['/', '/docs'] }, + parameters: { window: 60, urls: [] }, }; export default function FunnelReport({ reportId }) { diff --git a/components/pages/reports/funnel/AddUrlForm.js b/components/pages/reports/funnel/UrlAddForm.js similarity index 85% rename from components/pages/reports/funnel/AddUrlForm.js rename to components/pages/reports/funnel/UrlAddForm.js index 07f34867..b9d25f29 100644 --- a/components/pages/reports/funnel/AddUrlForm.js +++ b/components/pages/reports/funnel/UrlAddForm.js @@ -1,8 +1,9 @@ import { useState } from 'react'; import { useMessages } from 'hooks'; import { Button, Form, FormButtons, FormRow, TextField } from 'react-basics'; +import styles from './UrlAddForm.module.css'; -export function AddUrlForm({ defaultValue = '', onSave, onClose }) { +export function UrlAddForm({ defaultValue = '', onSave, onClose }) { const [url, setUrl] = useState(defaultValue); const { formatMessage, labels } = useMessages(); @@ -22,7 +23,7 @@ export function AddUrlForm({ defaultValue = '', onSave, onClose }) { }; return ( -
+ , + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + if (req.method === 'GET') { + const { websiteId, startAt, endAt } = req.query; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt)); + + return ok(res, data); + } + + if (req.method === 'POST') { + const { + websiteId, + dateRange: { startDate, endDate }, + } = req.body; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const data = {}; + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/queries/analytics/eventData/getEventDataFields.ts b/queries/analytics/eventData/getEventDataFields.ts new file mode 100644 index 00000000..5d2b9ef0 --- /dev/null +++ b/queries/analytics/eventData/getEventDataFields.ts @@ -0,0 +1,48 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; +import { WebsiteEventDataMetric } from 'lib/types'; +import { loadWebsite } from 'lib/query'; + +export async function getEventDataFields( + ...args: [websiteId: string, startDate: Date, endDate: Date] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, startDate: Date, endDate: Date) { + const { toUuid, rawQuery } = prisma; + const website = await loadWebsite(websiteId); + const resetDate = new Date(website?.resetAt || website?.createdAt); + const params: any = [websiteId, resetDate, startDate, endDate]; + + return rawQuery( + `select + distinct event_key as eventKey, data_type as eventDataType + from event_data + where website_id = $1${toUuid()} + and created_at >= $2 + and created_at between $3 and $4`, + params, + ); +} + +async function clickhouseQuery(websiteId: string, startDate: Date, endDate: Date) { + const { rawQuery, getDateFormat, getBetweenDates } = clickhouse; + const website = await loadWebsite(websiteId); + const resetDate = new Date(website?.resetAt || website?.createdAt); + const params = { websiteId }; + + return rawQuery( + `select + distinct event_key as eventKey, data_type as eventDataType + from event_data + where website_id = {websiteId:UUID} + and created_at >= ${getDateFormat(resetDate)} + and ${getBetweenDates('created_at', startDate, endDate)}`, + params, + ); +} diff --git a/queries/analytics/session/saveSessionData.ts b/queries/analytics/session/saveSessionData.ts index 76842b4f..beec27f7 100644 --- a/queries/analytics/session/saveSessionData.ts +++ b/queries/analytics/session/saveSessionData.ts @@ -37,7 +37,7 @@ export async function saveSessionData(data: { }, }), client.sessionData.createMany({ - data: flattendData, + data: flattendData as any, }), ]); } diff --git a/queries/index.js b/queries/index.js index 302c88db..8a4b42bf 100644 --- a/queries/index.js +++ b/queries/index.js @@ -7,6 +7,7 @@ export * from './analytics/event/getEventMetrics'; export * from './analytics/event/getEventUsage'; export * from './analytics/event/getEvents'; export * from './analytics/eventData/getEventData'; +export * from './analytics/eventData/getEventDataFields'; export * from './analytics/eventData/getEventDataUsage'; export * from './analytics/event/saveEvent'; export * from './analytics/pageview/getPageviewFunnel'; diff --git a/yarn.lock b/yarn.lock index e7d7e8e1..579d52af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8191,10 +8191,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.82.0: - version "0.82.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.82.0.tgz#1df241f4ef97a8d0c7c81d4954d065efc2c415a5" - integrity sha512-DrHuRJqncx7cWYFz4rAahEsCsF3s5W1CJyJY276EUphfn0NDKZcvnrSnr6G+KTUAEkvksvlAm1t3nDv85coUxg== +react-basics@^0.83.0: + version "0.83.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.83.0.tgz#bc4a962967383ecec20a0eb49b88c004c67a7f3a" + integrity sha512-3P74I1Tp8Ih8gw3xG65mB0aTzG0oTOYg72Gpfbd3igKjg/L1wgN6stWjIwzNP7mgWFc0FQ63BB13P7pL025bNQ== dependencies: classnames "^2.3.1" date-fns "^2.29.3"