mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Scaffolding for insights report.
This commit is contained in:
parent
2f4d669836
commit
827102d907
@ -41,7 +41,6 @@ export function EventDataParameters() {
|
|||||||
const parameterGroups = [
|
const parameterGroups = [
|
||||||
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
||||||
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
|
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
|
||||||
{ label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const parameterData = {
|
const parameterData = {
|
||||||
@ -55,11 +54,9 @@ export function EventDataParameters() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = (group, value) => {
|
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) => {
|
const handleRemove = (group, index) => {
|
||||||
@ -127,11 +124,6 @@ export function EventDataParameters() {
|
|||||||
<div>{value[1]}</div>
|
<div>{value[1]}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{group === REPORT_PARAMETERS.groups && (
|
|
||||||
<>
|
|
||||||
<div>{name}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -3,12 +3,12 @@ import ReportHeader from '../ReportHeader';
|
|||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../ReportBody';
|
||||||
import EventDataParameters from './EventDataParameters';
|
import EventDataParameters from './EventDataParameters';
|
||||||
import Nodes from 'assets/nodes.svg';
|
|
||||||
import EventDataTable from './EventDataTable';
|
import EventDataTable from './EventDataTable';
|
||||||
|
import Nodes from 'assets/nodes.svg';
|
||||||
|
|
||||||
const defaultParameters = {
|
const defaultParameters = {
|
||||||
type: 'event-data',
|
type: 'event-data',
|
||||||
parameters: { fields: [], filters: [], groups: [] },
|
parameters: { fields: [], filters: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EventDataReport({ reportId }) {
|
export default function EventDataReport({ reportId }) {
|
||||||
|
44
components/pages/reports/insights/FieldAddForm.js
Normal file
44
components/pages/reports/insights/FieldAddForm.js
Normal 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;
|
38
components/pages/reports/insights/FieldAddForm.module.css
Normal file
38
components/pages/reports/insights/FieldAddForm.module.css
Normal 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;
|
||||||
|
}
|
151
components/pages/reports/insights/InsightsParameters.js
Normal file
151
components/pages/reports/insights/InsightsParameters.js
Normal 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;
|
@ -0,0 +1,12 @@
|
|||||||
|
.parameter {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
26
components/pages/reports/insights/InsightsReport.js
Normal file
26
components/pages/reports/insights/InsightsReport.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
19
components/pages/reports/insights/InsightsTable.js
Normal file
19
components/pages/reports/insights/InsightsTable.js
Normal 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;
|
@ -40,16 +40,22 @@ async function clickhouseQuery(
|
|||||||
endDate: Date,
|
endDate: Date,
|
||||||
criteria: EventDataCriteria,
|
criteria: EventDataCriteria,
|
||||||
) {
|
) {
|
||||||
const { fields } = criteria;
|
const { fields, filters } = criteria;
|
||||||
const { rawQuery, getDateFormat, getBetweenDates } = clickhouse;
|
const { rawQuery, getDateFormat, getBetweenDates } = clickhouse;
|
||||||
const website = await loadWebsite(websiteId);
|
const website = await loadWebsite(websiteId);
|
||||||
const resetDate = new Date(website?.resetAt || DEFAULT_CREATED_AT);
|
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]) {
|
if (!obj[name]) {
|
||||||
obj[name] = {
|
obj[name] = {
|
||||||
columns: ['event_key as field', `count(*) as total`, `${type}_value as value`],
|
columns: [
|
||||||
groups: ['event_key', `${type}_value`],
|
'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;
|
return obj;
|
||||||
@ -69,7 +75,7 @@ async function clickhouseQuery(
|
|||||||
and created_at >= ${getDateFormat(resetDate)}
|
and created_at >= ${getDateFormat(resetDate)}
|
||||||
and ${getBetweenDates('created_at', startDate, endDate)}
|
and ${getBetweenDates('created_at', startDate, endDate)}
|
||||||
group by ${field.groups.join(',')}
|
group by ${field.groups.join(',')}
|
||||||
limit 20
|
limit 100
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user