diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index 2920280f..bf4d0aaa 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -1,16 +1,17 @@ +import { useRouter } from 'next/router'; import FilterLink from 'components/common/FilterLink'; import MetricsTable from 'components/metrics/MetricsTable'; -import { BROWSERS } from 'lib/constants'; import useMessages from 'hooks/useMessages'; -import { useRouter } from 'next/router'; +import useFormat from 'hooks/useFormat'; export function BrowsersTable({ websiteId, ...props }) { const { formatMessage, labels } = useMessages(); const { basePath } = useRouter(); + const { formatBrowser } = useFormat(); function renderLink({ x: browser }) { return ( - + {browser} {code} diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index 0b8d5708..98690d0a 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -2,18 +2,16 @@ import MetricsTable from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'hooks/useMessages'; import { useRouter } from 'next/router'; +import { useFormat } from 'hooks'; export function DevicesTable({ websiteId, ...props }) { const { formatMessage, labels } = useMessages(); const { basePath } = useRouter(); + const { formatDevice } = useFormat(); function renderLink({ x: device }) { return ( - + {device} - onSelect(fields[key])}> - {fields.map(({ label, name, type }, index) => { + onSelect(items[key])}> + {items.map(({ name, label, type }, index) => { return (
{label || name}
diff --git a/components/pages/reports/FilterSelectForm.js b/components/pages/reports/FilterSelectForm.js index 0dc107b0..29493b08 100644 --- a/components/pages/reports/FilterSelectForm.js +++ b/components/pages/reports/FilterSelectForm.js @@ -2,11 +2,11 @@ import { useState } from 'react'; import FieldSelectForm from './FieldSelectForm'; import FieldFilterForm from './FieldFilterForm'; -export default function FilterSelectForm({ fields, onSelect }) { +export default function FilterSelectForm({ items, onSelect }) { const [field, setField] = useState(); if (!field) { - return ; + return ; } return ; diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js index 5d7e1fca..4ec60a9a 100644 --- a/components/pages/reports/insights/InsightsParameters.js +++ b/components/pages/reports/insights/InsightsParameters.js @@ -2,7 +2,6 @@ import { useContext, useRef } from 'react'; import { useMessages } from 'hooks'; import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; import { ReportContext } from 'components/pages/reports/Report'; -import { REPORT_PARAMETERS } from 'lib/constants'; import Icons from 'components/icons'; import BaseParameters from '../BaseParameters'; import ParameterList from '../ParameterList'; @@ -16,52 +15,52 @@ export function InsightsParameters() { const { formatMessage, labels } = useMessages(); const ref = useRef(null); const { parameters } = report || {}; - const { websiteId, dateRange, filters, groups } = parameters || {}; - const queryEnabled = websiteId && dateRange && (filters?.length || groups?.length); + const { websiteId, dateRange, fields, filters } = parameters || {}; + const queryEnabled = websiteId && dateRange && (fields?.length || filters?.length); const fieldOptions = [ - { name: 'url_path', type: 'string', label: formatMessage(labels.url) }, - { name: 'page_title', type: 'string', label: formatMessage(labels.pageTitle) }, - { name: 'referrer_domain', type: 'string', label: formatMessage(labels.referrer) }, - { name: 'url_query', type: 'string', label: formatMessage(labels.query) }, - { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, - { name: 'os', type: 'string', label: formatMessage(labels.os) }, - { name: 'device', type: 'string', label: formatMessage(labels.device) }, - { name: 'country', type: 'string', label: formatMessage(labels.country) }, - { name: 'region', type: 'string', label: formatMessage(labels.region) }, - { name: 'city', type: 'string', label: formatMessage(labels.city) }, - { name: 'language', type: 'string', label: formatMessage(labels.language) }, + { name: 'url_path', label: formatMessage(labels.url) }, + { name: 'page_title', label: formatMessage(labels.pageTitle) }, + { name: 'referrer_domain', label: formatMessage(labels.referrer) }, + { name: 'url_query', label: formatMessage(labels.query) }, + { name: 'browser', label: formatMessage(labels.browser) }, + { name: 'os', label: formatMessage(labels.os) }, + { name: 'device', label: formatMessage(labels.device) }, + { name: 'country', label: formatMessage(labels.country) }, + { name: 'region', label: formatMessage(labels.region) }, + { name: 'city', label: formatMessage(labels.city) }, + { name: 'language', label: formatMessage(labels.language) }, ]; const parameterGroups = [ - { label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups }, - { label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters }, + { id: 'fields', label: formatMessage(labels.fields) }, + { id: 'filters', label: formatMessage(labels.filters) }, ]; const parameterData = { + fields, filters, - groups, }; const handleSubmit = values => { runReport(values); }; - const handleAdd = (group, value) => { - const data = parameterData[group]; + const handleAdd = (id, value) => { + const data = parameterData[id]; if (!data.find(({ name }) => name === value.name)) { - updateReport({ parameters: { [group]: data.concat(value) } }); + updateReport({ parameters: { [id]: data.concat(value) } }); } }; - const handleRemove = (group, index) => { - const data = [...parameterData[group]]; + const handleRemove = (id, index) => { + const data = [...parameterData[id]]; data.splice(index, 1); - updateReport({ parameters: { [group]: data } }); + updateReport({ parameters: { [id]: data } }); }; - const AddButton = ({ group }) => { + const AddButton = ({ id }) => { return ( @@ -71,11 +70,11 @@ export function InsightsParameters() { {(close, element) => { return ( - {group === REPORT_PARAMETERS.groups && ( - + {id === 'fields' && ( + )} - {group === REPORT_PARAMETERS.filters && ( - + {id === 'filters' && ( + )} ); @@ -88,22 +87,19 @@ export function InsightsParameters() { return (
- {parameterGroups.map(({ label, group }) => { + {parameterGroups.map(({ id, label }) => { return ( - }> - handleRemove(group, index)} - > + }> + handleRemove(id, index)}> {({ value, label }) => { return (
- {group === REPORT_PARAMETERS.groups && ( + {id === 'fields' && ( <>
{label}
)} - {group === REPORT_PARAMETERS.filters && ( + {id === 'filters' && ( <>
{label}
{value[0]}
diff --git a/components/pages/reports/insights/InsightsTable.js b/components/pages/reports/insights/InsightsTable.js index 7832a899..0d5298e4 100644 --- a/components/pages/reports/insights/InsightsTable.js +++ b/components/pages/reports/insights/InsightsTable.js @@ -1,20 +1,42 @@ -import { useContext } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { GridTable, GridColumn } from 'react-basics'; -import { useMessages } from 'hooks'; +import { useFormat, useMessages } from 'hooks'; import { ReportContext } from '../Report'; +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; export function InsightsTable() { + const [fields, setFields] = useState(); const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const { groups = [] } = report?.parameters || {}; + const { formatValue } = useFormat(); + + useEffect( + () => { + setFields(report?.parameters?.fields); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [report?.data], + ); + + if (!fields) { + return ; + } return ( - {groups.map(({ name, label }) => { - return ; + {fields.map(({ name, label }) => { + return ( + + {row => formatValue(row[name], name)} + + ); })} - - + + {row => row.visitors.toLocaleString()} + + + {row => row.views.toLocaleString()} + ); } diff --git a/hooks/index.js b/hooks/index.js index 6a9b3b35..004260b0 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -6,6 +6,7 @@ export * from './useDocumentClick'; export * from './useEscapeKey'; export * from './useFilters'; export * from './useForceUpdate'; +export * from './useFormat'; export * from './useLanguageNames'; export * from './useLocale'; export * from './useMessages'; diff --git a/hooks/useFormat.js b/hooks/useFormat.js new file mode 100644 index 00000000..3fd10ec8 --- /dev/null +++ b/hooks/useFormat.js @@ -0,0 +1,39 @@ +import useMessages from './useMessages'; +import { BROWSERS } from 'lib/constants'; +import useLocale from './useLocale'; +import useCountryNames from './useCountryNames'; + +export function useFormat() { + const { formatMessage, labels } = useMessages(); + const { locale } = useLocale(); + const countryNames = useCountryNames(locale); + + const formatBrowser = value => { + return BROWSERS[value] || value; + }; + + const formatCountry = value => { + return countryNames[value] || value; + }; + + const formatDevice = value => { + return formatMessage(labels[value] || labels.unknown); + }; + + const formatValue = (value, type) => { + switch (type) { + case 'browser': + return formatBrowser(value); + case 'country': + return formatCountry(value); + case 'device': + return formatDevice(value); + default: + return value; + } + }; + + return { formatBrowser, formatCountry, formatDevice, formatValue }; +} + +export default useFormat; diff --git a/hooks/useMessages.js b/hooks/useMessages.js index 0719afd8..3c13fab0 100644 --- a/hooks/useMessages.js +++ b/hooks/useMessages.js @@ -4,11 +4,11 @@ import { messages, labels } from 'components/messages'; export function useMessages() { const { formatMessage } = useIntl(); - function getMessage(id) { + const getMessage = id => { const message = Object.values(messages).find(value => value.id === id); return message ? formatMessage(message) : id; - } + }; return { formatMessage, FormattedMessage, messages, labels, getMessage }; } diff --git a/lib/data.ts b/lib/data.ts index c2c53de3..47023bb4 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -25,7 +25,7 @@ export function flattenJSON( ).keyValues; } -export function getDynamicDataType(value: any): string { +export function getDataType(value: any): string { let type: string = typeof value; if ((type === 'string' && isValid(value)) || isValid(parseISO(value))) { @@ -36,7 +36,7 @@ export function getDynamicDataType(value: any): string { } function createKey(key, value, acc: { keyValues: any[]; parentKey: string }) { - const type = getDynamicDataType(value); + const type = getDataType(value); let dynamicDataType = null; diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts index 44f72063..decb1f81 100644 --- a/pages/api/reports/insights.ts +++ b/pages/api/reports/insights.ts @@ -27,7 +27,7 @@ export default async ( const { websiteId, dateRange: { startDate, endDate }, - groups, + fields, filters, } = req.body; @@ -35,7 +35,7 @@ export default async ( return unauthorized(res); } - const data = await getInsights(websiteId, groups, { + const data = await getInsights(websiteId, fields, { ...filters, startDate: new Date(startDate), endDate: new Date(endDate), diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index a3a19bb1..25084111 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -25,7 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { event_data.event_key as "fieldName", event_data.data_type as "dataType", event_data.string_value as "value", - count(*) as total + count(*) as "total" from event_data inner join website_event on website_event.event_id = event_data.website_event_id diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts index 0f778555..4c47052b 100644 --- a/queries/analytics/reports/getInsights.ts +++ b/queries/analytics/reports/getInsights.ts @@ -5,7 +5,7 @@ import { EVENT_TYPE } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getInsights( - ...args: [websiteId: string, groups: { name: string; type: string }[], filters: QueryFilters] + ...args: [websiteId: string, fields: { name: string; type?: string }[], filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -15,7 +15,7 @@ export async function getInsights( async function relationalQuery( websiteId: string, - groups: { name: string; type: string }[], + fields: { name: string; type?: string }[], filters: QueryFilters, ): Promise< { @@ -41,6 +41,7 @@ async function relationalQuery( and website_event.event_type = {{eventType}} ${filterQuery} group by 1 + limit 500 `, params, ); @@ -48,7 +49,7 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - groups: { name: string; type: string }[], + fields: { name: string; type?: string }[], filters: QueryFilters, ): Promise< { @@ -65,14 +66,14 @@ async function clickhouseQuery( return rawQuery( ` select - ${parseFields(groups)} + ${parseFields(fields)} from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} and event_type = {eventType:UInt32} ${filterQuery} - group by ${groups.map(({ name }) => name).join(',')} - order by 1 desc + group by ${fields.map(({ name }) => name).join(',')} + order by 1 desc, 2 desc limit 500 `, params,