diff --git a/components/common/NoData.js b/components/common/Empty.js similarity index 57% rename from components/common/NoData.js rename to components/common/Empty.js index e9c95754..95681b16 100644 --- a/components/common/NoData.js +++ b/components/common/Empty.js @@ -1,15 +1,15 @@ import classNames from 'classnames'; -import styles from './NoData.module.css'; +import styles from './Empty.module.css'; import useMessages from 'hooks/useMessages'; -export function NoData({ className }) { +export function Empty({ message, className }) { const { formatMessage, messages } = useMessages(); return (
- {formatMessage(messages.noDataAvailable)} + {message || formatMessage(messages.noDataAvailable)}
); } -export default NoData; +export default Empty; diff --git a/components/common/NoData.module.css b/components/common/Empty.module.css similarity index 100% rename from components/common/NoData.module.css rename to components/common/Empty.module.css diff --git a/components/messages.js b/components/messages.js index 9499e2a6..016d3c4c 100644 --- a/components/messages.js +++ b/components/messages.js @@ -129,7 +129,6 @@ export const labels = defineMessages({ urls: { id: 'label.urls', defaultMessage: 'URLs' }, add: { id: 'label.add', defaultMessage: 'Add' }, window: { id: 'label.window', defaultMessage: 'Window' }, - addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' }, runQuery: { id: 'label.run-query', defaultMessage: 'Run query' }, field: { id: 'label.field', defaultMessage: 'Field' }, fields: { id: 'label.fields', defaultMessage: 'Fields' }, @@ -137,6 +136,20 @@ export const labels = defineMessages({ description: { id: 'labels.description', defaultMessage: 'Description' }, untitled: { id: 'labels.untitled', defaultMessage: 'Untitled' }, type: { id: 'labels.type', defaultMessage: 'Type' }, + filters: { id: 'labels.filters', defaultMessage: 'Filters' }, + groupBy: { id: 'labels.group-by', defaultMessage: 'Group by' }, + true: { id: 'labels.true', defaultMessage: 'True' }, + false: { id: 'labels.false', defaultMessage: 'False' }, + equals: { id: 'labels.equals', defaultMessage: 'Equals' }, + doesNotEqual: { id: 'labels.does-not-equal', defaultMessage: 'Does not equal' }, + greaterThan: { id: 'labels.greater-than', defaultMessage: 'Greater than' }, + lessThan: { id: 'labels.less-than', defaultMessage: 'Less than' }, + greaterThanEquals: { id: 'labels.greater-than-equals', defaultMessage: 'Greater than or equals' }, + lessThanEquals: { id: 'labels.less-than-equals', defaultMessage: 'Less than or equals' }, + contains: { id: 'labels.contains', defaultMessage: 'Contains' }, + doesNotContain: { id: 'labels.does-not-contain', defaultMessage: 'Does not contain' }, + before: { id: 'labels.before', defaultMessage: 'Before' }, + after: { id: 'labels.after', defaultMessage: 'After' }, }); export const messages = defineMessages({ @@ -244,4 +257,8 @@ export const messages = defineMessages({ id: 'message.incorrect-username-password', defaultMessage: 'Incorrect username and/or password.', }, + noEventData: { + id: 'message.no-event-data', + defaultMessage: 'No event data is available.', + }, }); diff --git a/components/metrics/DataTable.js b/components/metrics/DataTable.js index a2c1a568..e2e9462d 100644 --- a/components/metrics/DataTable.js +++ b/components/metrics/DataTable.js @@ -3,7 +3,7 @@ import useMeasure from 'react-use-measure'; import { FixedSizeList } from 'react-window'; import { useSpring, animated, config } from 'react-spring'; import classNames from 'classnames'; -import NoData from 'components/common/NoData'; +import Empty from 'components/common/Empty'; import { formatNumber, formatLongNumber } from 'lib/format'; import useMessages from 'hooks/useMessages'; import styles from './DataTable.module.css'; @@ -55,7 +55,7 @@ export function DataTable({
- {data?.length === 0 && } + {data?.length === 0 && } {virtualize && data.length > 0 ? ( {Row} diff --git a/components/pages/realtime/RealtimeLog.js b/components/pages/realtime/RealtimeLog.js index ddd35751..fe12963c 100644 --- a/components/pages/realtime/RealtimeLog.js +++ b/components/pages/realtime/RealtimeLog.js @@ -3,7 +3,7 @@ import { StatusLight, Icon, Text } from 'react-basics'; import { FixedSizeList } from 'react-window'; import firstBy from 'thenby'; import FilterButtons from 'components/common/FilterButtons'; -import NoData from 'components/common/NoData'; +import Empty from 'components/common/Empty'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; @@ -144,7 +144,7 @@ export function RealtimeLog({ data, websiteDomain }) {
{formatMessage(labels.activityLog)}
- {logs?.length === 0 && } + {logs?.length === 0 && } {logs?.length > 0 && ( {Row} diff --git a/components/pages/reports/FieldAggregateForm.js b/components/pages/reports/FieldAggregateForm.js new file mode 100644 index 00000000..f4298c16 --- /dev/null +++ b/components/pages/reports/FieldAggregateForm.js @@ -0,0 +1,38 @@ +import { Form, FormRow, Menu, Item } from 'react-basics'; + +const options = { + number: [ + { label: 'SUM', value: 'sum' }, + { label: 'AVERAGE', value: 'average' }, + { label: 'MIN', value: 'min' }, + { label: 'MAX', value: 'max' }, + ], + date: [ + { label: 'MIN', value: 'min' }, + { label: 'MAX', value: 'max' }, + ], + string: [ + { label: 'COUNT', value: 'count' }, + { label: 'DISTINCT', value: 'distinct' }, + ], +}; + +export default function FieldAggregateForm({ name, type, onSelect }) { + const items = options[type]; + + const handleSelect = value => { + onSelect({ name, value }); + }; + + return ( +
+ + + {items.map(({ label, value }) => { + return {label}; + })} + + +
+ ); +} diff --git a/components/pages/reports/FieldFilterForm.js b/components/pages/reports/FieldFilterForm.js new file mode 100644 index 00000000..e4272c69 --- /dev/null +++ b/components/pages/reports/FieldFilterForm.js @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import { Form, FormRow, Menu, Item, Flexbox, Dropdown, TextField, Button } from 'react-basics'; +import { useFilters } from 'hooks'; +import styles from './FieldFilterForm.module.css'; + +export default function FieldFilterForm({ name, type, onSelect }) { + const [filter, setFilter] = useState(''); + const [value, setValue] = useState(''); + const { filters, types } = useFilters(); + const items = types[type]; + + const renderValue = value => { + return filters[value]; + }; + + if (type === 'boolean') { + return ( +
+ + onSelect({ name, value: ['eq', value] })}> + {items.map(value => { + return {filters[value]}; + })} + + +
+ ); + } + + return ( +
+ + + + {value => { + return {filters[value]}; + }} + + setValue(e.target.value)} autoFocus={true} /> + + + +
+ ); +} diff --git a/components/pages/reports/FieldFilterForm.module.css b/components/pages/reports/FieldFilterForm.module.css new file mode 100644 index 00000000..f0cc46f3 --- /dev/null +++ b/components/pages/reports/FieldFilterForm.module.css @@ -0,0 +1,17 @@ +.selected { + font-weight: bold; +} + +.popup { + display: flex; +} + +.filter { + display: flex; + flex-direction: column; + gap: 20px; +} + +.dropdown { + min-width: 60px; +} diff --git a/components/pages/reports/FieldSelectForm.js b/components/pages/reports/FieldSelectForm.js new file mode 100644 index 00000000..1ff6412a --- /dev/null +++ b/components/pages/reports/FieldSelectForm.js @@ -0,0 +1,24 @@ +import { Menu, Item, Form, FormRow } from 'react-basics'; +import { useMessages } from 'hooks'; +import styles from './FieldSelectForm.module.css'; + +export default function FieldSelectForm({ fields, onSelect }) { + const { formatMessage, labels } = useMessages(); + + return ( +
+ + onSelect(fields[key])}> + {fields.map(({ name, type }, index) => { + return ( + +
{name}
+
{type}
+
+ ); + })} +
+
+
+ ); +} diff --git a/components/pages/reports/FieldSelectForm.module.css b/components/pages/reports/FieldSelectForm.module.css new file mode 100644 index 00000000..3a5ed9b8 --- /dev/null +++ b/components/pages/reports/FieldSelectForm.module.css @@ -0,0 +1,20 @@ +.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); +} diff --git a/components/pages/reports/ParameterList.js b/components/pages/reports/ParameterList.js new file mode 100644 index 00000000..604f6223 --- /dev/null +++ b/components/pages/reports/ParameterList.js @@ -0,0 +1,33 @@ +import { Icon, Text, TooltipPopup } from 'react-basics'; +import Icons from 'components/icons'; +import Empty from 'components/common/Empty'; +import { useMessages } from 'hooks'; +import styles from './ParameterList.module.css'; + +export function ParameterList({ items = [], children, onRemove }) { + const { formatMessage, labels } = useMessages(); + + return ( +
+ {!items.length && } + {items.map((item, index) => { + return ( +
+ {typeof children === 'function' ? children(item) : item} + + + + + +
+ ); + })} +
+ ); +} + +export default ParameterList; diff --git a/components/pages/reports/funnel/FunnelParameters.module.css b/components/pages/reports/ParameterList.module.css similarity index 95% rename from components/pages/reports/funnel/FunnelParameters.module.css rename to components/pages/reports/ParameterList.module.css index 98ae0831..601b37e5 100644 --- a/components/pages/reports/funnel/FunnelParameters.module.css +++ b/components/pages/reports/ParameterList.module.css @@ -1,10 +1,10 @@ -.urls { +.list { display: flex; flex-direction: column; gap: 16px; } -.url { +.item { display: flex; flex-direction: row; align-items: center; diff --git a/components/pages/reports/PopupForm.js b/components/pages/reports/PopupForm.js new file mode 100644 index 00000000..0f0ead36 --- /dev/null +++ b/components/pages/reports/PopupForm.js @@ -0,0 +1,30 @@ +import { createPortal } from 'react-dom'; +import { useDocumentClick, useKeyDown } from 'react-basics'; +import classNames from 'classnames'; +import styles from './PopupForm.module.css'; + +export function PopupForm({ element, className, children, onClose }) { + const { right, top } = element.getBoundingClientRect(); + const style = { position: 'absolute', left: right, top }; + + useKeyDown('Escape', onClose); + + useDocumentClick(e => { + if (e.target !== element && !element?.parentElement?.contains(e.target)) { + onClose(); + } + }); + + const handleClick = e => { + e.stopPropagation(); + }; + + return createPortal( +
+ {children} +
, + document.body, + ); +} + +export default PopupForm; diff --git a/components/pages/reports/PopupForm.module.css b/components/pages/reports/PopupForm.module.css new file mode 100644 index 00000000..4daf199a --- /dev/null +++ b/components/pages/reports/PopupForm.module.css @@ -0,0 +1,10 @@ +.form { + position: absolute; + background: var(--base50); + min-width: 300px; + padding: 20px; + margin-left: 30px; + border: 1px solid var(--base400); + border-radius: var(--border-radius); + box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1); +} diff --git a/components/pages/reports/Report.js b/components/pages/reports/Report.js index 0a98ef75..685ebb9f 100644 --- a/components/pages/reports/Report.js +++ b/components/pages/reports/Report.js @@ -8,6 +8,8 @@ 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/ReportDetails.js b/components/pages/reports/ReportDetails.js new file mode 100644 index 00000000..c41d12f6 --- /dev/null +++ b/components/pages/reports/ReportDetails.js @@ -0,0 +1,13 @@ +import FunnelReport from './funnel/FunnelReport'; +import EventDataReport from './event-data/EventDataReport'; + +const reports = { + funnel: FunnelReport, + 'event-data': EventDataReport, +}; + +export default function ReportDetails({ reportId, reportType }) { + const Report = reports[reportType]; + + return ; +} diff --git a/components/pages/reports/event-data/EventDataParameters.js b/components/pages/reports/event-data/EventDataParameters.js index 8255ae92..c703f9bf 100644 --- a/components/pages/reports/event-data/EventDataParameters.js +++ b/components/pages/reports/event-data/EventDataParameters.js @@ -1,17 +1,25 @@ import { useContext, useRef } from 'react'; import { useApi, useMessages } from 'hooks'; -import { Form, FormRow, FormButtons, SubmitButton, Loading } from 'react-basics'; +import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; import { ReportContext } from 'components/pages/reports/Report'; -import NoData from 'components/common/NoData'; -import styles from './EventDataParameters.module.css'; +import Empty from 'components/common/Empty'; import { DATA_TYPES } from 'lib/constants'; import BaseParameters from '../BaseParameters'; +import FieldAddForm from './FieldAddForm'; +import ParameterList from '../ParameterList'; +import Icons from 'components/icons'; +import styles from './EventDataParameters.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 }), + () => + get('/reports/event-data', { + websiteId, + startAt: +startDate, + endAt: +endDate, + }), { enabled: !!(websiteId && startDate && endDate) }, ); @@ -19,35 +27,111 @@ function useFields(websiteId, startDate, endDate) { } export function EventDataParameters() { - const { report, runReport, isRunning } = useContext(ReportContext); - const { formatMessage, labels } = useMessages(); + const { report, runReport, updateReport, isRunning } = useContext(ReportContext); + const { formatMessage, labels, messages } = useMessages(); const ref = useRef(null); const { parameters } = report || {}; - const { websiteId, dateRange } = parameters || {}; + const { websiteId, dateRange, fields, filters, groups } = parameters || {}; const { startDate, endDate } = dateRange || {}; const queryDisabled = !websiteId || !dateRange; - const { data, error, isLoading } = useFields(websiteId, startDate, endDate); + const { data, error } = useFields(websiteId, startDate, endDate); + const parametersSelected = websiteId && startDate && endDate; + const hasData = data?.length !== 0; + + const parameterGroups = [ + { label: formatMessage(labels.fields), type: 'fields' }, + { label: formatMessage(labels.filters), type: 'filters' }, + { label: formatMessage(labels.groupBy), type: 'groups' }, + ]; + + const parameterData = { + fields, + filters, + groups, + }; const handleSubmit = values => { runReport(values); }; + const handleAdd = (type, value) => { + const data = parameterData[type]; + updateReport({ parameters: { [type]: data.concat(value) } }); + }; + + const handleRemove = (type, index) => { + const data = [...parameterData[type]]; + data.splice(index, 1); + updateReport({ parameters: { [type]: data } }); + }; + + const AddButton = ({ type }) => { + return ( + + + + + + {(close, element) => { + return ( + ({ + name: eventKey, + type: DATA_TYPES[eventDataType], + }))} + element={element} + onAdd={handleAdd} + onClose={close} + /> + ); + }} + + + ); + }; + return (
- -
- {!data?.length && } - {data?.map?.(({ eventKey, eventDataType }) => { - return ( -
-
{eventKey}
-
{DATA_TYPES[eventDataType]}
-
- ); - })} -
-
+ {!hasData && } + {parametersSelected && + hasData && + parameterGroups.map(({ label, type }) => { + return ( + }> + handleRemove(type, index)} + > + {({ name, value }) => { + return ( +
+ {type === 'fields' && ( + <> +
{value}
+
{name}
+ + )} + {type === 'filters' && ( + <> +
{name}
+
{value[0]}
+
{value[1]}
+ + )} + {type === 'groups' && ( + <> +
{name}
+ + )} +
+ ); + }} +
+
+ ); + })} {formatMessage(labels.runQuery)} diff --git a/components/pages/reports/event-data/EventDataParameters.module.css b/components/pages/reports/event-data/EventDataParameters.module.css index 66c82842..435cb1f6 100644 --- a/components/pages/reports/event-data/EventDataParameters.module.css +++ b/components/pages/reports/event-data/EventDataParameters.module.css @@ -1,27 +1,8 @@ -.fields { - max-height: 300px; - overflow: auto; -} - -.field { +.parameter { display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - height: 30px; - cursor: pointer; - padding: 4px; - border-radius: var(--border-radius); + gap: 10px; } -.field:hover { - background: var(--base75); -} - -.key { - font-weight: 400; -} - -.type { - color: var(--font-color300); +.op { + font-weight: bold; } diff --git a/components/pages/reports/event-data/EventDataReport.js b/components/pages/reports/event-data/EventDataReport.js index 16382982..b3358ecf 100644 --- a/components/pages/reports/event-data/EventDataReport.js +++ b/components/pages/reports/event-data/EventDataReport.js @@ -4,10 +4,11 @@ import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; import EventDataParameters from './EventDataParameters'; import Nodes from 'assets/nodes.svg'; +import EventDataTable from './EventDataTable'; const defaultParameters = { type: 'event-data', - parameters: { fields: [], filters: [] }, + parameters: { fields: [], filters: [], groups: [] }, }; export default function EventDataReport({ reportId }) { @@ -17,7 +18,9 @@ export default function EventDataReport({ reportId }) { - hi. + + + ); } diff --git a/components/pages/reports/event-data/EventDataTable.js b/components/pages/reports/event-data/EventDataTable.js new file mode 100644 index 00000000..ffe9fb3a --- /dev/null +++ b/components/pages/reports/event-data/EventDataTable.js @@ -0,0 +1,20 @@ +import { useContext } from 'react'; +import DataTable from 'components/metrics/DataTable'; +import { useMessages } from 'hooks'; +import { ReportContext } from '../Report'; + +export function EventDataTable() { + const { report } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + + return ( + + ); +} + +export default EventDataTable; diff --git a/components/pages/reports/event-data/FieldAddForm.js b/components/pages/reports/event-data/FieldAddForm.js index b0258608..df71b370 100644 --- a/components/pages/reports/event-data/FieldAddForm.js +++ b/components/pages/reports/event-data/FieldAddForm.js @@ -1,27 +1,35 @@ -import { useMessages } from 'hooks'; -import { Button, Form, FormButtons, FormRow } from 'react-basics'; +import { useState } from 'react'; +import { createPortal } from 'react-dom'; +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({ onClose }) { - const { formatMessage, labels } = useMessages(); +export function FieldAddForm({ fields = [], type, element, onAdd, onClose }) { + const [selected, setSelected] = useState(); - const handleSave = () => { + const handleSelect = value => { + if (type === 'groups') { + handleSave(value); + return; + } + + setSelected(value); + }; + + const handleSave = value => { + onAdd(type, value); onClose(); }; - const handleClose = () => { - onClose(); - }; - - return ( - - - - - - - + return createPortal( + + {!selected && } + {selected && type === 'fields' && } + {selected && type === 'filters' && } + , + document.body, ); } diff --git a/components/pages/reports/event-data/FieldAddForm.module.css b/components/pages/reports/event-data/FieldAddForm.module.css new file mode 100644 index 00000000..5c5aaa4f --- /dev/null +++ b/components/pages/reports/event-data/FieldAddForm.module.css @@ -0,0 +1,38 @@ +.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; +} diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js index 5ce671a8..ae498176 100644 --- a/components/pages/reports/funnel/FunnelParameters.js +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -9,15 +9,13 @@ import { PopupTrigger, Popup, SubmitButton, - Text, TextField, - TooltipPopup, } from 'react-basics'; import Icons from 'components/icons'; import UrlAddForm from './UrlAddForm'; import { ReportContext } from 'components/pages/reports/Report'; -import styles from './FunnelParameters.module.css'; import BaseParameters from '../BaseParameters'; +import ParameterList from '../ParameterList'; export function FunnelParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); @@ -28,7 +26,9 @@ export function FunnelParameters() { const { websiteId, dateRange, urls } = parameters || {}; const queryDisabled = !websiteId || !dateRange || urls?.length < 2; - const handleSubmit = data => { + const handleSubmit = (data, e) => { + e.stopPropagation(); + e.preventDefault(); if (!queryDisabled) { runReport(data); } @@ -45,8 +45,23 @@ export function FunnelParameters() { updateReport({ parameters: { urls } }); }; + const AddUrlButton = () => { + return ( + + + + + + {(close, element) => { + return ; + }} + + + ); + }; + return ( -
+ - }> -
- {parameters?.urls?.map((url, index) => { - return ( -
- {url} - - - - - -
- ); - })} -
+ }> + @@ -85,25 +83,4 @@ export function FunnelParameters() { ); } -function AddUrlButton({ onAdd }) { - const { formatMessage, labels } = useMessages(); - - return ( - - - - - - - - {(close, element) => { - const { right, bottom } = element.getBoundingClientRect(); - - return ; - }} - - - ); -} - export default FunnelParameters; diff --git a/components/pages/reports/funnel/UrlAddForm.js b/components/pages/reports/funnel/UrlAddForm.js index b1ee37f0..0fb78b3d 100644 --- a/components/pages/reports/funnel/UrlAddForm.js +++ b/components/pages/reports/funnel/UrlAddForm.js @@ -1,16 +1,15 @@ import { useState } from 'react'; -import { createPortal } from 'react-dom'; import { useMessages } from 'hooks'; import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics'; import styles from './UrlAddForm.module.css'; +import PopupForm from '../PopupForm'; -export function UrlAddForm({ defaultValue = '', style, onSave, onClose }) { +export function UrlAddForm({ defaultValue = '', element, onAdd, onClose }) { const [url, setUrl] = useState(defaultValue); const { formatMessage, labels } = useMessages(); - const handleSave = e => { - e?.stopPropagation?.(); - onSave?.(url); + const handleSave = () => { + onAdd(url); setUrl(''); onClose(); }; @@ -19,29 +18,33 @@ export function UrlAddForm({ defaultValue = '', style, onSave, onClose }) { setUrl(e.target.value); }; - const handleClick = e => { - e.stopPropagation(); + const handleKeyDown = e => { + if (e.key === 'Enter') { + e.stopPropagation(); + handleSave(); + } }; - return createPortal( - - - - - - - - , - document.body, + return ( + +
+ + + + + + +
+
); } diff --git a/hooks/index.js b/hooks/index.js index cca6ca29..892d52e4 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -4,6 +4,7 @@ export * from './useCountryNames'; export * from './useDateRange'; export * from './useDocumentClick'; export * from './useEscapeKey'; +export * from './useFilters'; export * from './useForceUpdate'; export * from './useLanguageNames'; export * from './useLocale'; diff --git a/hooks/useFilters.js b/hooks/useFilters.js new file mode 100644 index 00000000..ae01aadb --- /dev/null +++ b/hooks/useFilters.js @@ -0,0 +1,32 @@ +import { useMessages } from 'hooks'; + +export function useFilters() { + const { formatMessage, labels } = useMessages(); + + const filters = { + eq: formatMessage(labels.equals), + neq: formatMessage(labels.doesNotEqual), + c: formatMessage(labels.contains), + dnc: formatMessage(labels.doesNotContain), + t: formatMessage(labels.true), + f: formatMessage(labels.false), + gt: formatMessage(labels.greaterThan), + lt: formatMessage(labels.lessThan), + gte: formatMessage(labels.greaterThanEquals), + lte: formatMessage(labels.lessThanEquals), + be: formatMessage(labels.before), + af: formatMessage(labels.after), + }; + + const types = { + string: ['eq', 'neq'], + array: ['c', 'dnc'], + boolean: ['t', 'f'], + number: ['eq', 'neq', 'gt', 'lt', 'gte', 'lte'], + date: ['be', 'af'], + }; + + return { filters, types }; +} + +export default useFilters; diff --git a/hooks/useReport.js b/hooks/useReport.js index 5bd5624f..e036fc3a 100644 --- a/hooks/useReport.js +++ b/hooks/useReport.js @@ -16,6 +16,14 @@ export function useReport(reportId, defaultParameters) { const loadReport = async id => { const data = await get(`/reports/${id}`); + const { dateRange } = data?.parameters || {}; + const { startDate, endDate } = dateRange || {}; + + if (startDate && endDate) { + dateRange.startDate = new Date(startDate); + dateRange.endDate = new Date(endDate); + } + setReport(data); }; diff --git a/lib/constants.ts b/lib/constants.ts index 15b2444d..16f119fb 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -62,7 +62,7 @@ export const EVENT_TYPE = { customEvent: 2, } as const; -export const DYNAMIC_DATA_TYPE = { +export const DATA_TYPE = { string: 1, number: 2, boolean: 3, @@ -71,11 +71,11 @@ export const DYNAMIC_DATA_TYPE = { } as const; export const DATA_TYPES = { - [DYNAMIC_DATA_TYPE.string]: 'string', - [DYNAMIC_DATA_TYPE.number]: 'number', - [DYNAMIC_DATA_TYPE.boolean]: 'boolean', - [DYNAMIC_DATA_TYPE.date]: 'date', - [DYNAMIC_DATA_TYPE.array]: 'array', + [DATA_TYPE.string]: 'string', + [DATA_TYPE.number]: 'number', + [DATA_TYPE.boolean]: 'boolean', + [DATA_TYPE.date]: 'date', + [DATA_TYPE.array]: 'array', }; export const KAFKA_TOPIC = { diff --git a/lib/dynamicData.ts b/lib/dynamicData.ts index da8eb8b2..c2c53de3 100644 --- a/lib/dynamicData.ts +++ b/lib/dynamicData.ts @@ -1,5 +1,5 @@ import { isValid, parseISO } from 'date-fns'; -import { DYNAMIC_DATA_TYPE } from './constants'; +import { DATA_TYPE } from './constants'; import { DynamicDataType } from './types'; export function flattenJSON( @@ -42,24 +42,24 @@ function createKey(key, value, acc: { keyValues: any[]; parentKey: string }) { switch (type) { case 'number': - dynamicDataType = DYNAMIC_DATA_TYPE.number; + dynamicDataType = DATA_TYPE.number; break; case 'string': - dynamicDataType = DYNAMIC_DATA_TYPE.string; + dynamicDataType = DATA_TYPE.string; break; case 'boolean': - dynamicDataType = DYNAMIC_DATA_TYPE.boolean; + dynamicDataType = DATA_TYPE.boolean; value = value ? 'true' : 'false'; break; case 'date': - dynamicDataType = DYNAMIC_DATA_TYPE.date; + dynamicDataType = DATA_TYPE.date; break; case 'object': - dynamicDataType = DYNAMIC_DATA_TYPE.array; + dynamicDataType = DATA_TYPE.array; value = JSON.stringify(value); break; default: - dynamicDataType = DYNAMIC_DATA_TYPE.string; + dynamicDataType = DATA_TYPE.string; break; } diff --git a/lib/types.ts b/lib/types.ts index 05c09120..5841ac35 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import { NextApiRequest } from 'next'; -import { COLLECTION_TYPE, DYNAMIC_DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants'; +import { COLLECTION_TYPE, DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants'; type ObjectValues = T[keyof T]; @@ -9,7 +9,7 @@ export type Role = ObjectValues; export type EventType = ObjectValues; -export type DynamicDataType = ObjectValues; +export type DynamicDataType = ObjectValues; export type KafkaTopic = ObjectValues; diff --git a/pages/reports/[id].js b/pages/reports/[id].js index 95f20a78..36a84a2e 100644 --- a/pages/reports/[id].js +++ b/pages/reports/[id].js @@ -1,20 +1,24 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; -import FunnelReport from 'components/pages/reports/funnel/FunnelReport'; -import useMessages from 'hooks/useMessages'; +import ReportDetails from 'components/pages/reports/ReportDetails'; +import { useApi, useMessages } from 'hooks'; export default function ReportsPage() { const { formatMessage, labels } = useMessages(); const router = useRouter(); const { id } = router.query; + const { get, useQuery } = useApi(); + const { data: report } = useQuery(['reports', id], () => get(`/reports/${id}`), { + enabled: !!id, + }); - if (!id) { + if (!id || !report) { return null; } return ( - + ); } diff --git a/public/intl/messages/ru-RU.json b/public/intl/messages/ru-RU.json index 6048d7e9..d15b9fac 100644 --- a/public/intl/messages/ru-RU.json +++ b/public/intl/messages/ru-RU.json @@ -230,7 +230,7 @@ "label.edit": [ { "type": 0, - "value": "Редактировать" + "value": "Изменить" } ], "label.edit-dashboard": [ @@ -820,15 +820,7 @@ "message.delete-website": [ { "type": 0, - "value": "To delete this website, type " - }, - { - "type": 1, - "value": "confirmation" - }, - { - "type": 0, - "value": " in the box below to confirm." + "value": "Для удаления введите DELETE" } ], "message.delete-website-warning": [ @@ -922,15 +914,7 @@ "message.reset-website": [ { "type": 0, - "value": "To reset this website, type " - }, - { - "type": 1, - "value": "confirmation" - }, - { - "type": 0, - "value": " in the box below to confirm." + "value": "Для сброса введите RESET" } ], "message.reset-website-warning": [ diff --git a/public/intl/messages/sv-SE.json b/public/intl/messages/sv-SE.json index 59e9721a..dc9ee53c 100644 --- a/public/intl/messages/sv-SE.json +++ b/public/intl/messages/sv-SE.json @@ -14,7 +14,7 @@ "label.activity-log": [ { "type": 0, - "value": "Activity log" + "value": "Aktivitetslogg" } ], "label.add-website": [ @@ -44,7 +44,7 @@ "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "Analys" } ], "label.average-visit-time": [ @@ -86,19 +86,19 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Städer" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "Rensa alla" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Bekräfta" } ], "label.confirm-password": [ @@ -110,7 +110,7 @@ "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Fortsätt" } ], "label.countries": [ @@ -122,19 +122,19 @@ "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Skapa team" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Skapa användare" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Skapad" } ], "label.current-password": [ @@ -182,13 +182,13 @@ "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Radera team" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Radera användare" } ], "label.delete-website": [ @@ -206,7 +206,7 @@ "label.details": [ { "type": 0, - "value": "Details" + "value": "Detailjer" } ], "label.devices": [ @@ -236,7 +236,7 @@ "label.edit-dashboard": [ { "type": 0, - "value": "Edit dashboard" + "value": "Redigera översikt" } ], "label.enable-share-url": [ @@ -278,13 +278,13 @@ "label.join": [ { "type": 0, - "value": "Join" + "value": "Gå med" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "gå med i team" } ], "label.language": [ @@ -336,13 +336,13 @@ "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Lämna" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Lämna team" } ], "label.login": [ @@ -360,7 +360,7 @@ "label.members": [ { "type": 0, - "value": "Members" + "value": "Medlemmar" } ], "label.mobile": [ @@ -390,7 +390,7 @@ "label.none": [ { "type": 0, - "value": "None" + "value": "Inga" } ], "label.operating-systems": [ @@ -442,19 +442,19 @@ "label.queries": [ { "type": 0, - "value": "Queries" + "value": "Frågor" } ], "label.query": [ { "type": 0, - "value": "Query" + "value": "Frågor" } ], "label.query-parameters": [ { "type": 0, - "value": "Query parameters" + "value": "Fråge-parametrar" } ], "label.realtime": [ @@ -478,19 +478,19 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Regenerera" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Regioner" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Ta bort" } ], "label.reports": [ @@ -520,7 +520,7 @@ "label.role": [ { "type": 0, - "value": "Role" + "value": "Roll" } ], "label.save": [ @@ -532,7 +532,7 @@ "label.screens": [ { "type": 0, - "value": "Screens" + "value": "Upplösning" } ], "label.select-date": [ @@ -544,7 +544,7 @@ "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Välj webbsajt" } ], "label.sessions": [ @@ -586,7 +586,7 @@ "label.team-guest": [ { "type": 0, - "value": "Team guest" + "value": "Team-gäst" } ], "label.team-id": [ @@ -598,19 +598,19 @@ "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Team-medlem" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Team-ägare" } ], "label.teams": [ { "type": 0, - "value": "Teams" + "value": "Team" } ], "label.theme": [ @@ -646,7 +646,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "Titel" } ], "label.today": [ @@ -688,7 +688,7 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Användare" } ], "label.username": [ @@ -706,7 +706,7 @@ "label.view": [ { "type": 0, - "value": "View" + "value": "Visa" } ], "label.view-details": [ @@ -736,7 +736,7 @@ "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "Webbsajt-ID" } ], "label.websites": [ @@ -748,7 +748,7 @@ "label.yesterday": [ { "type": 0, - "value": "Yesterday" + "value": "Igår" } ], "message.active-users": [ @@ -806,7 +806,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Är du säker på att du vill lämna " }, { "type": 1, @@ -878,7 +878,7 @@ }, { "type": 0, - "value": " on " + "value": " på " }, { "type": 1, @@ -906,7 +906,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Minst " }, { "type": 1, @@ -914,7 +914,7 @@ }, { "type": 0, - "value": " characters" + "value": " tecken" } ], "message.no-data-available": [ @@ -932,13 +932,13 @@ "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Du har inte skapat några team." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Det finns inga användare." } ], "message.page-not-found": [ @@ -950,7 +950,7 @@ "message.reset-website": [ { "type": 0, - "value": "To reset this website, type " + "value": "För att återställa statistiken skriv " }, { "type": 1, @@ -958,7 +958,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " i rutan nedan." } ], "message.reset-website-warning": [ @@ -990,13 +990,13 @@ "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Du är redan medlem i teamet." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Team kan inte hittas." } ], "message.tracking-code": [ @@ -1008,7 +1008,7 @@ "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Användare raderad." } ], "message.visitor-log": [ @@ -1054,7 +1054,7 @@ "messages.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Det här teamet har inga webbsajter." } ], "messages.no-websites-configured": [ @@ -1066,7 +1066,7 @@ "messages.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Websajter kan ses av alla i teamet." } ] } diff --git a/queries/analytics/eventData/getEventDataFields.ts b/queries/analytics/eventData/getEventDataFields.ts index 43c3363a..dc7f20c2 100644 --- a/queries/analytics/eventData/getEventDataFields.ts +++ b/queries/analytics/eventData/getEventDataFields.ts @@ -26,7 +26,8 @@ async function relationalQuery(websiteId: string, startDate: Date, endDate: Date from event_data where website_id = $1${toUuid()} and created_at >= $2 - and created_at between $3 and $4`, + and created_at between $3 and $4 + order by event_key asc`, params, ); } @@ -43,7 +44,8 @@ async function clickhouseQuery(websiteId: string, startDate: Date, endDate: Date from event_data where website_id = {websiteId:UUID} and created_at >= ${getDateFormat(resetDate)} - and ${getBetweenDates('created_at', startDate, endDate)}`, + and ${getBetweenDates('created_at', startDate, endDate)} + order by event_key asc`, params, ); } diff --git a/queries/analytics/eventData/saveEventData.ts b/queries/analytics/eventData/saveEventData.ts index 96ea8831..e9f8d21e 100644 --- a/queries/analytics/eventData/saveEventData.ts +++ b/queries/analytics/eventData/saveEventData.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client'; -import { DYNAMIC_DATA_TYPE } from 'lib/constants'; +import { DATA_TYPE } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { flattenJSON } from 'lib/dynamicData'; @@ -38,13 +38,13 @@ async function relationalQuery(data: { websiteId, key: a.key, stringValue: - a.dynamicDataType === DYNAMIC_DATA_TYPE.string || - a.dynamicDataType === DYNAMIC_DATA_TYPE.boolean || - a.dynamicDataType === DYNAMIC_DATA_TYPE.array + a.dynamicDataType === DATA_TYPE.string || + a.dynamicDataType === DATA_TYPE.boolean || + a.dynamicDataType === DATA_TYPE.array ? a.value : null, - numericValue: a.dynamicDataType === DYNAMIC_DATA_TYPE.number ? a.value : null, - dateValue: a.dynamicDataType === DYNAMIC_DATA_TYPE.date ? new Date(a.value) : null, + numericValue: a.dynamicDataType === DATA_TYPE.number ? a.value : null, + dateValue: a.dynamicDataType === DATA_TYPE.date ? new Date(a.value) : null, dataType: a.dynamicDataType, })); @@ -76,13 +76,13 @@ async function clickhouseQuery(data: { event_name: eventName, event_key: a.key, string_value: - a.dynamicDataType === DYNAMIC_DATA_TYPE.string || - a.dynamicDataType === DYNAMIC_DATA_TYPE.boolean || - a.dynamicDataType === DYNAMIC_DATA_TYPE.array + a.dynamicDataType === DATA_TYPE.string || + a.dynamicDataType === DATA_TYPE.boolean || + a.dynamicDataType === DATA_TYPE.array ? a.value : null, - numeric_value: a.dynamicDataType === DYNAMIC_DATA_TYPE.number ? a.value : null, - date_value: a.dynamicDataType === DYNAMIC_DATA_TYPE.date ? getDateFormat(a.value) : null, + numeric_value: a.dynamicDataType === DATA_TYPE.number ? a.value : null, + date_value: a.dynamicDataType === DATA_TYPE.date ? getDateFormat(a.value) : null, data_type: a.dynamicDataType, created_at: createdAt, })); diff --git a/queries/analytics/session/saveSessionData.ts b/queries/analytics/session/saveSessionData.ts index beec27f7..79104270 100644 --- a/queries/analytics/session/saveSessionData.ts +++ b/queries/analytics/session/saveSessionData.ts @@ -1,4 +1,4 @@ -import { DYNAMIC_DATA_TYPE } from 'lib/constants'; +import { DATA_TYPE } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { flattenJSON } from 'lib/dynamicData'; import prisma from 'lib/prisma'; @@ -20,13 +20,13 @@ export async function saveSessionData(data: { sessionId, key: a.key, stringValue: - a.dynamicDataType === DYNAMIC_DATA_TYPE.string || - a.dynamicDataType === DYNAMIC_DATA_TYPE.boolean || - a.dynamicDataType === DYNAMIC_DATA_TYPE.array + a.dynamicDataType === DATA_TYPE.string || + a.dynamicDataType === DATA_TYPE.boolean || + a.dynamicDataType === DATA_TYPE.array ? a.value : null, - numericValue: a.dynamicDataType === DYNAMIC_DATA_TYPE.number ? a.value : null, - dateValue: a.dynamicDataType === DYNAMIC_DATA_TYPE.date ? new Date(a.value) : null, + numericValue: a.dynamicDataType === DATA_TYPE.number ? a.value : null, + dateValue: a.dynamicDataType === DATA_TYPE.date ? new Date(a.value) : null, dataType: a.dynamicDataType, }));