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 (
+
+ );
+}
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 (
+
+ );
+ }
+
+ return (
+
+ );
+}
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 (
+
+ );
+}
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 (