Scaffolding for insights report.

This commit is contained in:
Mike Cao 2023-07-07 20:38:43 -07:00
parent 2f4d669836
commit 827102d907
9 changed files with 305 additions and 17 deletions

View File

@ -41,7 +41,6 @@ export function EventDataParameters() {
const parameterGroups = [
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
{ label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups },
];
const parameterData = {
@ -55,11 +54,9 @@ export function EventDataParameters() {
};
const handleAdd = (group, value) => {
const data = parameterData[group];
const data = parameterData[group].filter(({ name }) => name !== value.name);
if (!data.find(({ name }) => name === value.name)) {
updateReport({ parameters: { [group]: data.concat(value) } });
}
updateReport({ parameters: { [group]: data.concat(value) } });
};
const handleRemove = (group, index) => {
@ -127,11 +124,6 @@ export function EventDataParameters() {
<div>{value[1]}</div>
</>
)}
{group === REPORT_PARAMETERS.groups && (
<>
<div>{name}</div>
</>
)}
</div>
);
}}

View File

@ -3,12 +3,12 @@ import ReportHeader from '../ReportHeader';
import ReportMenu from '../ReportMenu';
import ReportBody from '../ReportBody';
import EventDataParameters from './EventDataParameters';
import Nodes from 'assets/nodes.svg';
import EventDataTable from './EventDataTable';
import Nodes from 'assets/nodes.svg';
const defaultParameters = {
type: 'event-data',
parameters: { fields: [], filters: [], groups: [] },
parameters: { fields: [], filters: [] },
};
export default function EventDataReport({ reportId }) {

View File

@ -0,0 +1,44 @@
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { REPORT_PARAMETERS } from 'lib/constants';
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({ fields = [], group, element, onAdd, onClose }) {
const [selected, setSelected] = useState();
const handleSelect = value => {
const { type } = value;
if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
value.value = group === REPORT_PARAMETERS.groups ? '' : 'total';
handleSave(value);
return;
}
setSelected(value);
};
const handleSave = value => {
onAdd(group, value);
onClose();
};
return createPortal(
<PopupForm className={styles.popup} element={element} onClose={onClose}>
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
{selected && group === REPORT_PARAMETERS.fields && (
<FieldAggregateForm {...selected} onSelect={handleSave} />
)}
{selected && group === REPORT_PARAMETERS.filters && (
<FieldFilterForm {...selected} onSelect={handleSave} />
)}
</PopupForm>,
document.body,
);
}
export default FieldAddForm;

View File

@ -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;
}

View File

@ -0,0 +1,151 @@
import { useContext, useRef } from 'react';
import { useApi, useMessages } from 'hooks';
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
import { ReportContext } from 'components/pages/reports/Report';
import Empty from 'components/common/Empty';
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
import Icons from 'components/icons';
import FieldAddForm from './FieldAddForm';
import BaseParameters from '../BaseParameters';
import ParameterList from '../ParameterList';
import styles from './InsightsParameters.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,
}),
{ enabled: !!(websiteId && startDate && endDate) },
);
return { data, error, isLoading };
}
export function InsightsParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels, messages } = useMessages();
const ref = useRef(null);
const { parameters } = report || {};
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
const { startDate, endDate } = dateRange || {};
const queryEnabled = websiteId && dateRange && fields?.length;
const { data, error } = useFields(websiteId, startDate, endDate);
const parametersSelected = websiteId && startDate && endDate;
const hasData = data?.length !== 0;
const parameterGroups = [
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
{ label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups },
];
const parameterData = {
fields,
filters,
groups,
};
const handleSubmit = values => {
runReport(values);
};
const handleAdd = (group, value) => {
const data = parameterData[group];
if (!data.find(({ name }) => name === value.name)) {
updateReport({ parameters: { [group]: data.concat(value) } });
}
};
const handleRemove = (group, index) => {
const data = [...parameterData[group]];
data.splice(index, 1);
updateReport({ parameters: { [group]: data } });
};
const AddButton = ({ group }) => {
return (
<PopupTrigger>
<Icon>
<Icons.Plus />
</Icon>
<Popup position="bottom" alignment="start">
{(close, element) => {
return (
<FieldAddForm
fields={data.map(({ eventKey, InsightsType }) => ({
name: eventKey,
type: DATA_TYPES[InsightsType],
}))}
group={group}
element={element}
onAdd={handleAdd}
onClose={close}
/>
);
}}
</Popup>
</PopupTrigger>
);
};
return (
<Form ref={ref} values={parameters} error={error} onSubmit={handleSubmit}>
<BaseParameters />
{!hasData && <Empty message={formatMessage(messages.noInsights)} />}
{parametersSelected &&
hasData &&
parameterGroups.map(({ label, group }) => {
return (
<FormRow
key={label}
label={label}
action={<AddButton group={group} onAdd={handleAdd} />}
>
<ParameterList
items={parameterData[group]}
onRemove={index => handleRemove(group, index)}
>
{({ name, value }) => {
return (
<div className={styles.parameter}>
{group === REPORT_PARAMETERS.fields && (
<>
<div>{name}</div>
<div className={styles.op}>{value}</div>
</>
)}
{group === REPORT_PARAMETERS.filters && (
<>
<div>{name}</div>
<div className={styles.op}>{value[0]}</div>
<div>{value[1]}</div>
</>
)}
{group === REPORT_PARAMETERS.groups && (
<>
<div>{name}</div>
</>
)}
</div>
);
}}
</ParameterList>
</FormRow>
);
})}
<FormButtons>
<SubmitButton variant="primary" disabled={!queryEnabled} loading={isRunning}>
{formatMessage(labels.runQuery)}
</SubmitButton>
</FormButtons>
</Form>
);
}
export default InsightsParameters;

View File

@ -0,0 +1,12 @@
.parameter {
display: flex;
gap: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-width: 0;
}
.op {
font-weight: bold;
}

View File

@ -0,0 +1,26 @@
import Report from '../Report';
import ReportHeader from '../ReportHeader';
import ReportMenu from '../ReportMenu';
import ReportBody from '../ReportBody';
import InsightsParameters from './InsightsParameters';
import InsightsTable from './InsightsTable';
import Lightbulb from 'assets/lightbulb.svg';
const defaultParameters = {
type: 'insights',
parameters: { fields: [], filters: [], groups: [] },
};
export default function InsightsReport({ reportId }) {
return (
<Report reportId={reportId} defaultParameters={defaultParameters}>
<ReportHeader icon={<Lightbulb />} />
<ReportMenu>
<InsightsParameters />
</ReportMenu>
<ReportBody>
<InsightsTable />
</ReportBody>
</Report>
);
}

View File

@ -0,0 +1,19 @@
import { useContext } from 'react';
import { GridTable, GridColumn } from 'react-basics';
import { useMessages } from 'hooks';
import { ReportContext } from '../Report';
export function InsightsTable() {
const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
return (
<GridTable data={report?.data || []}>
<GridColumn name="field" label={formatMessage(labels.field)} />
<GridColumn name="value" label={formatMessage(labels.value)} />
<GridColumn name="total" label={formatMessage(labels.total)} />
</GridTable>
);
}
export default InsightsTable;

View File

@ -40,16 +40,22 @@ async function clickhouseQuery(
endDate: Date,
criteria: EventDataCriteria,
) {
const { fields } = criteria;
const { fields, filters } = criteria;
const { rawQuery, getDateFormat, getBetweenDates } = clickhouse;
const website = await loadWebsite(websiteId);
const resetDate = new Date(website?.resetAt || DEFAULT_CREATED_AT);
const uniqueFields = fields.reduce((obj, { name, type }) => {
const uniqueFields = fields.reduce((obj, { name, type, value }) => {
const prefix = type === 'array' ? 'string' : type;
if (!obj[name]) {
obj[name] = {
columns: ['event_key as field', `count(*) as total`, `${type}_value as value`],
groups: ['event_key', `${type}_value`],
columns: [
'event_key as field',
`count(*) as total`,
value === 'unique' ? `${prefix}_value as value` : null,
].filter(n => n),
groups: ['event_key', value === 'unique' ? `${prefix}_value` : null].filter(n => n),
};
}
return obj;
@ -69,7 +75,7 @@ async function clickhouseQuery(
and created_at >= ${getDateFormat(resetDate)}
and ${getBetweenDates('created_at', startDate, endDate)}
group by ${field.groups.join(',')}
limit 20
limit 100
`,
params,
),