mirror of
https://github.com/umami-software/umami.git
synced 2026-02-11 08:07:12 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
9a7fb1f36c
20 changed files with 266 additions and 102 deletions
|
|
@ -1,5 +1,13 @@
|
||||||
import { useRef, useState } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Text, Icon, CalendarMonthSelect, CalendarYearSelect, Button } from 'react-basics';
|
import {
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
CalendarMonthSelect,
|
||||||
|
CalendarYearSelect,
|
||||||
|
Button,
|
||||||
|
PopupTrigger,
|
||||||
|
Popup,
|
||||||
|
} from 'react-basics';
|
||||||
import { startOfMonth, endOfMonth } from 'date-fns';
|
import { startOfMonth, endOfMonth } from 'date-fns';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import { useLocale } from 'hooks';
|
import { useLocale } from 'hooks';
|
||||||
|
|
@ -7,43 +15,50 @@ import { formatDate } from 'lib/date';
|
||||||
import { getDateLocale } from 'lib/lang';
|
import { getDateLocale } from 'lib/lang';
|
||||||
import styles from './MonthSelect.module.css';
|
import styles from './MonthSelect.module.css';
|
||||||
|
|
||||||
const MONTH = 'month';
|
|
||||||
const YEAR = 'year';
|
|
||||||
|
|
||||||
export function MonthSelect({ date = new Date(), onChange }) {
|
export function MonthSelect({ date = new Date(), onChange }) {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const [select, setSelect] = useState(null);
|
|
||||||
const month = formatDate(date, 'MMMM', locale);
|
const month = formatDate(date, 'MMMM', locale);
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
|
||||||
const handleSelect = value => {
|
|
||||||
setSelect(state => (state !== value ? value : null));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = date => {
|
const handleChange = date => {
|
||||||
onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`);
|
onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`);
|
||||||
setSelect(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={ref} className={styles.container}>
|
<div ref={ref} className={styles.container}>
|
||||||
<Button className={styles.input} variant="quiet" onClick={() => handleSelect(MONTH)}>
|
<PopupTrigger>
|
||||||
<Text>{month}</Text>
|
<Button className={styles.input} variant="quiet">
|
||||||
<Icon size="sm">{select === MONTH ? <Icons.Close /> : <Icons.ChevronDown />}</Icon>
|
<Text>{month}</Text>
|
||||||
</Button>
|
<Icon size="sm">
|
||||||
<Button className={styles.input} variant="quiet" onClick={() => handleSelect(YEAR)}>
|
<Icons.ChevronDown />
|
||||||
<Text>{year}</Text>
|
</Icon>
|
||||||
<Icon size="sm">{select === YEAR ? <Icons.Close /> : <Icons.ChevronDown />}</Icon>
|
</Button>
|
||||||
</Button>
|
<Popup className={styles.popup} alignment="start">
|
||||||
|
<CalendarMonthSelect
|
||||||
|
date={date}
|
||||||
|
locale={getDateLocale(locale)}
|
||||||
|
onSelect={handleChange}
|
||||||
|
/>
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
|
<PopupTrigger>
|
||||||
|
<Button className={styles.input} variant="quiet">
|
||||||
|
<Text>{year}</Text>
|
||||||
|
<Icon size="sm">
|
||||||
|
<Icons.ChevronDown />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
<Popup className={styles.popup} alignment="start">
|
||||||
|
<CalendarYearSelect
|
||||||
|
date={date}
|
||||||
|
locale={getDateLocale(locale)}
|
||||||
|
onSelect={handleChange}
|
||||||
|
/>
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
</div>
|
</div>
|
||||||
{select === MONTH && (
|
|
||||||
<CalendarMonthSelect date={date} locale={getDateLocale(locale)} onSelect={handleChange} />
|
|
||||||
)}
|
|
||||||
{select === YEAR && (
|
|
||||||
<CalendarYearSelect date={date} locale={getDateLocale(locale)} onSelect={handleChange} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
border: 1px solid var(--base400);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
|
@ -10,3 +12,11 @@
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
border: 1px solid var(--base400);
|
||||||
|
background: var(--base50);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1 / 2;
|
grid-row: 1 / 2;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,11 @@
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { useDocumentClick, useKeyDown } from 'react-basics';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './PopupForm.module.css';
|
import styles from './PopupForm.module.css';
|
||||||
|
|
||||||
export function PopupForm({ element, className, children, onClose }) {
|
export function PopupForm({ className, style, children }) {
|
||||||
const { right, top } = element.getBoundingClientRect();
|
return (
|
||||||
const style = { position: 'absolute', left: right, top };
|
<div className={classNames(styles.form, className)} style={style}>
|
||||||
|
|
||||||
useKeyDown('Escape', onClose);
|
|
||||||
|
|
||||||
useDocumentClick(e => {
|
|
||||||
if (e.target !== element && !element?.parentElement?.contains(e.target)) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClick = e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<div className={classNames(styles.form, className)} style={style} onClick={handleClick}>
|
|
||||||
{children}
|
{children}
|
||||||
</div>,
|
</div>
|
||||||
document.body,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
background: var(--base50);
|
background: var(--base50);
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-left: 30px;
|
|
||||||
border: 1px solid var(--base400);
|
border: 1px solid var(--base400);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,10 @@ export function InsightsParameters() {
|
||||||
<Icons.Plus />
|
<Icons.Plus />
|
||||||
</Icon>
|
</Icon>
|
||||||
</TooltipPopup>
|
</TooltipPopup>
|
||||||
<Popup position="bottom" alignment="start">
|
<Popup position="bottom" alignment="start" className={styles.popup}>
|
||||||
{(close, element) => {
|
{close => {
|
||||||
return (
|
return (
|
||||||
<PopupForm element={element} onClose={close}>
|
<PopupForm onClose={close}>
|
||||||
{id === 'fields' && (
|
{id === 'fields' && (
|
||||||
<FieldSelectForm
|
<FieldSelectForm
|
||||||
items={fieldOptions}
|
items={fieldOptions}
|
||||||
|
|
@ -114,7 +114,7 @@ export function InsightsParameters() {
|
||||||
return (
|
return (
|
||||||
<FormRow key={label} label={label} action={<AddButton id={id} onAdd={handleAdd} />}>
|
<FormRow key={label} label={label} action={<AddButton id={id} onAdd={handleAdd} />}>
|
||||||
<ParameterList items={parameterData[id]} onRemove={index => handleRemove(id, index)}>
|
<ParameterList items={parameterData[id]} onRemove={index => handleRemove(id, index)}>
|
||||||
{({ name, filter, value, label }) => {
|
{({ name, filter, value }) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.parameter}>
|
<div className={styles.parameter}>
|
||||||
{id === 'fields' && (
|
{id === 'fields' && (
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,8 @@
|
||||||
.op {
|
.op {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export function RetentionParameters() {
|
||||||
return (
|
return (
|
||||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||||
<BaseParameters showDateSelect={false} />
|
<BaseParameters showDateSelect={false} />
|
||||||
<FormRow label={formatMessage(labels.dateRange)}>
|
<FormRow label={formatMessage(labels.date)}>
|
||||||
<MonthSelect date={startDate} onChange={handleDateChange} />
|
<MonthSelect date={startDate} onChange={handleDateChange} />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,16 @@ import ReportMenu from '../ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../ReportBody';
|
||||||
import Magnet from 'assets/magnet.svg';
|
import Magnet from 'assets/magnet.svg';
|
||||||
import { REPORT_TYPES } from 'lib/constants';
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
|
import { parseDateRange } from 'lib/date';
|
||||||
|
import { endOfMonth, startOfMonth } from 'date-fns';
|
||||||
|
|
||||||
const defaultParameters = {
|
const defaultParameters = {
|
||||||
type: REPORT_TYPES.retention,
|
type: REPORT_TYPES.retention,
|
||||||
parameters: {},
|
parameters: {
|
||||||
|
dateRange: parseDateRange(
|
||||||
|
`range:${startOfMonth(new Date()).getTime()}:${endOfMonth(new Date()).getTime()}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RetentionReport({ reportId }) {
|
export default function RetentionReport({ reportId }) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { GridTable, GridColumn } from 'react-basics';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../Report';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
|
|
@ -16,14 +15,26 @@ export function RetentionTable() {
|
||||||
return <EmptyPlaceholder />;
|
return <EmptyPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = data.reduce((arr, { date, visitors }) => {
|
const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
|
||||||
if (!arr.find(a => a.date === date)) {
|
|
||||||
return arr.concat({ date, visitors });
|
const rows = data.reduce((arr, row) => {
|
||||||
|
const { date, visitors, day } = row;
|
||||||
|
if (day === 0) {
|
||||||
|
return arr.concat({
|
||||||
|
date,
|
||||||
|
visitors,
|
||||||
|
records: days
|
||||||
|
.reduce((arr, day) => {
|
||||||
|
arr[day] = data.find(x => x.date === date && x.day === day);
|
||||||
|
return arr;
|
||||||
|
}, [])
|
||||||
|
.filter(n => n),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 30];
|
const totalDays = rows.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -37,15 +48,22 @@ export function RetentionTable() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{rows.map(({ date, visitors }, i) => {
|
{rows.map(({ date, visitors, records }, rowIndex) => {
|
||||||
return (
|
return (
|
||||||
<div key={i} className={styles.row}>
|
<div key={rowIndex} className={styles.row}>
|
||||||
<div className={styles.date}>{formatDate(`${date} 00:00:00`, 'PP')}</div>
|
<div className={styles.date}>{formatDate(`${date} 00:00:00`, 'PP')}</div>
|
||||||
<div className={styles.visitors}>{visitors}</div>
|
<div className={styles.visitors}>{visitors}</div>
|
||||||
{days.map((n, day) => {
|
{days.map(day => {
|
||||||
|
if (totalDays - rowIndex < day) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const percentage = records[day]?.percentage;
|
||||||
return (
|
return (
|
||||||
<div key={day} className={styles.cell}>
|
<div
|
||||||
{data.find(row => row.date === date && row.day === day)?.percentage.toFixed(2)}
|
key={day}
|
||||||
|
className={classNames(styles.cell, { [styles.empty]: !percentage })}
|
||||||
|
>
|
||||||
|
{percentage ? `${percentage.toFixed(2)}%` : ''}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -53,31 +71,8 @@ export function RetentionTable() {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<DataTable data={data} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataTable({ data }) {
|
|
||||||
return (
|
|
||||||
<GridTable data={data || []}>
|
|
||||||
<GridColumn name="date" label={'Date'}>
|
|
||||||
{row => row.date}
|
|
||||||
</GridColumn>
|
|
||||||
<GridColumn name="day" label={'Day'}>
|
|
||||||
{row => row.day}
|
|
||||||
</GridColumn>
|
|
||||||
<GridColumn name="visitors" label={'Visitors'}>
|
|
||||||
{row => row.visitors}
|
|
||||||
</GridColumn>
|
|
||||||
<GridColumn name="returnVisitors" label={'Return Visitors'}>
|
|
||||||
{row => row.returnVisitors}
|
|
||||||
</GridColumn>
|
|
||||||
<GridColumn name="percentage" label={'Percentage'}>
|
|
||||||
{row => row.percentage}
|
|
||||||
</GridColumn>
|
|
||||||
</GridTable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RetentionTable;
|
export default RetentionTable;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background: var(--blue100);
|
background: var(--blue200);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,3 +46,7 @@
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
background: var(--blue100);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `event_data_website_id_created_at_idx` ON `event_data`(`website_id`, `created_at`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `event_data_website_id_created_at_event_key_idx` ON `event_data`(`website_id`, `created_at`, `event_key`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_idx` ON `session`(`website_id`, `created_at`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_hostname_idx` ON `session`(`website_id`, `created_at`, `hostname`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_browser_idx` ON `session`(`website_id`, `created_at`, `browser`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_os_idx` ON `session`(`website_id`, `created_at`, `os`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_device_idx` ON `session`(`website_id`, `created_at`, `device`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_screen_idx` ON `session`(`website_id`, `created_at`, `screen`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_language_idx` ON `session`(`website_id`, `created_at`, `language`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_country_idx` ON `session`(`website_id`, `created_at`, `country`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_subdivision1_idx` ON `session`(`website_id`, `created_at`, `subdivision1`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_website_id_created_at_city_idx` ON `session`(`website_id`, `created_at`, `city`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_website_id_created_at_url_path_idx` ON `website_event`(`website_id`, `created_at`, `url_path`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_website_id_created_at_url_query_idx` ON `website_event`(`website_id`, `created_at`, `url_query`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_website_id_created_at_referrer_domain_idx` ON `website_event`(`website_id`, `created_at`, `referrer_domain`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_website_id_created_at_page_title_idx` ON `website_event`(`website_id`, `created_at`, `page_title`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_website_id_created_at_event_name_idx` ON `website_event`(`website_id`, `created_at`, `event_name`);
|
||||||
|
|
@ -44,6 +44,16 @@ model Session {
|
||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
|
@@index([websiteId, createdAt])
|
||||||
|
@@index([websiteId, createdAt, hostname])
|
||||||
|
@@index([websiteId, createdAt, browser])
|
||||||
|
@@index([websiteId, createdAt, os])
|
||||||
|
@@index([websiteId, createdAt, device])
|
||||||
|
@@index([websiteId, createdAt, screen])
|
||||||
|
@@index([websiteId, createdAt, language])
|
||||||
|
@@index([websiteId, createdAt, country])
|
||||||
|
@@index([websiteId, createdAt, subdivision1])
|
||||||
|
@@index([websiteId, createdAt, city])
|
||||||
@@map("session")
|
@@map("session")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,6 +101,11 @@ model WebsiteEvent {
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
@@index([websiteId, createdAt])
|
@@index([websiteId, createdAt])
|
||||||
|
@@index([websiteId, createdAt, urlPath])
|
||||||
|
@@index([websiteId, createdAt, urlQuery])
|
||||||
|
@@index([websiteId, createdAt, referrerDomain])
|
||||||
|
@@index([websiteId, createdAt, pageTitle])
|
||||||
|
@@index([websiteId, createdAt, eventName])
|
||||||
@@index([websiteId, sessionId, createdAt])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
@@map("website_event")
|
@@map("website_event")
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +128,8 @@ model EventData {
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
@@index([websiteEventId])
|
@@index([websiteEventId])
|
||||||
@@index([websiteId, websiteEventId, createdAt])
|
@@index([websiteId, websiteEventId, createdAt])
|
||||||
|
@@index([websiteId, createdAt])
|
||||||
|
@@index([websiteId, createdAt, eventKey])
|
||||||
@@map("event_data")
|
@@map("event_data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_data_website_id_created_at_idx" ON "event_data"("website_id", "created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_data_website_id_created_at_event_key_idx" ON "event_data"("website_id", "created_at", "event_key");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_idx" ON "session"("website_id", "created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_hostname_idx" ON "session"("website_id", "created_at", "hostname");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_browser_idx" ON "session"("website_id", "created_at", "browser");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_os_idx" ON "session"("website_id", "created_at", "os");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_device_idx" ON "session"("website_id", "created_at", "device");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_screen_idx" ON "session"("website_id", "created_at", "screen");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_language_idx" ON "session"("website_id", "created_at", "language");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_country_idx" ON "session"("website_id", "created_at", "country");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_subdivision1_idx" ON "session"("website_id", "created_at", "subdivision1");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_created_at_city_idx" ON "session"("website_id", "created_at", "city");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_website_id_created_at_url_path_idx" ON "website_event"("website_id", "created_at", "url_path");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_website_id_created_at_url_query_idx" ON "website_event"("website_id", "created_at", "url_query");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_website_id_created_at_referrer_domain_idx" ON "website_event"("website_id", "created_at", "referrer_domain");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_website_id_created_at_page_title_idx" ON "website_event"("website_id", "created_at", "page_title");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_website_id_created_at_event_name_idx" ON "website_event"("website_id", "created_at", "event_name");
|
||||||
|
|
@ -44,6 +44,16 @@ model Session {
|
||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
|
@@index([websiteId, createdAt])
|
||||||
|
@@index([websiteId, createdAt, hostname])
|
||||||
|
@@index([websiteId, createdAt, browser])
|
||||||
|
@@index([websiteId, createdAt, os])
|
||||||
|
@@index([websiteId, createdAt, device])
|
||||||
|
@@index([websiteId, createdAt, screen])
|
||||||
|
@@index([websiteId, createdAt, language])
|
||||||
|
@@index([websiteId, createdAt, country])
|
||||||
|
@@index([websiteId, createdAt, subdivision1])
|
||||||
|
@@index([websiteId, createdAt, city])
|
||||||
@@map("session")
|
@@map("session")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,6 +101,11 @@ model WebsiteEvent {
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
@@index([websiteId, createdAt])
|
@@index([websiteId, createdAt])
|
||||||
|
@@index([websiteId, createdAt, urlPath])
|
||||||
|
@@index([websiteId, createdAt, urlQuery])
|
||||||
|
@@index([websiteId, createdAt, referrerDomain])
|
||||||
|
@@index([websiteId, createdAt, pageTitle])
|
||||||
|
@@index([websiteId, createdAt, eventName])
|
||||||
@@index([websiteId, sessionId, createdAt])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
@@map("website_event")
|
@@map("website_event")
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +127,8 @@ model EventData {
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
@@index([websiteEventId])
|
@@index([websiteEventId])
|
||||||
|
@@index([websiteId, createdAt])
|
||||||
|
@@index([websiteId, createdAt, eventKey])
|
||||||
@@map("event_data")
|
@@map("event_data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ function getDateFormat(date) {
|
||||||
function mapFilter(column, operator, name, type = 'String') {
|
function mapFilter(column, operator, name, type = 'String') {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case OPERATORS.equals:
|
case OPERATORS.equals:
|
||||||
return `${column} = {${name}:${type}`;
|
return `${column} = {${name}:${type}}`;
|
||||||
case OPERATORS.notEquals:
|
case OPERATORS.notEquals:
|
||||||
return `${column} != {${name}:${type}}`;
|
return `${column} != {${name}:${type}}`;
|
||||||
default:
|
default:
|
||||||
|
|
@ -94,13 +94,23 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
|
||||||
return query.join('\n');
|
return query.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFilters(filters = {}) {
|
||||||
|
return Object.keys(filters).reduce((obj, key) => {
|
||||||
|
const value = filters[key];
|
||||||
|
|
||||||
|
obj[key] = value?.value ?? value;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
|
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
|
||||||
const website = await loadWebsite(websiteId);
|
const website = await loadWebsite(websiteId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filterQuery: getFilterQuery(filters, options),
|
filterQuery: getFilterQuery(filters, options),
|
||||||
params: {
|
params: {
|
||||||
...filters,
|
...normalizeFilters(filters),
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate: maxDate(filters.startDate, website.resetAt),
|
startDate: maxDate(filters.startDate, website.resetAt),
|
||||||
websiteDomain: website.domain,
|
websiteDomain: website.domain,
|
||||||
|
|
|
||||||
|
|
@ -92,12 +92,12 @@ function getTimestampIntervalQuery(field: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapFilter(column, operator, name, type = 'String') {
|
function mapFilter(column, operator, name, type = 'varchar') {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case OPERATORS.equals:
|
case OPERATORS.equals:
|
||||||
return `${column} = {${name}:${type}`;
|
return `${column} = {{${name}::${type}}}`;
|
||||||
case OPERATORS.notEquals:
|
case OPERATORS.notEquals:
|
||||||
return `${column} != {${name}:${type}}`;
|
return `${column} != {{${name}::${type}}}`;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +161,7 @@ async function rawQuery(sql: string, data: object): Promise<any> {
|
||||||
return Promise.reject(new Error('Unknown database.'));
|
return Promise.reject(new Error('Unknown database.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*}}/g, (...args) => {
|
const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*\}\}/g, (...args) => {
|
||||||
const [, name, type] = args;
|
const [, name, type] = args;
|
||||||
params.push(data[name]);
|
params.push(data[name]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-basics": "^0.94.0",
|
"react-basics": "^0.96.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^4.0.4",
|
"react-error-boundary": "^4.0.4",
|
||||||
|
|
|
||||||
|
|
@ -57,12 +57,14 @@ async function relationalQuery(
|
||||||
from level${i} l
|
from level${i} l
|
||||||
join website_event we
|
join website_event we
|
||||||
on l.session_id = we.session_id
|
on l.session_id = we.session_id
|
||||||
where we.created_at between l.created_at
|
where we.website_id = {{websiteId::uuid}}
|
||||||
and ${getAddMinutesQuery(`l.created_at `, windowMinutes)}
|
and we.created_at between l.created_at and ${getAddMinutesQuery(
|
||||||
|
`l.created_at `,
|
||||||
|
windowMinutes,
|
||||||
|
)}
|
||||||
and we.referrer_path = {{${i - 1}}}
|
and we.referrer_path = {{${i - 1}}}
|
||||||
and we.url_path = {{${i}}}
|
and we.url_path = {{${i}}}
|
||||||
and we.created_at <= {{endDate}}
|
and we.created_at <= {{endDate}}
|
||||||
and we.website_id = {{websiteId::uuid}}
|
|
||||||
)`;
|
)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7557,10 +7557,10 @@ rc@^1.2.7:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
react-basics@^0.94.0:
|
react-basics@^0.96.0:
|
||||||
version "0.94.0"
|
version "0.96.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.94.0.tgz#c15698148b959f40c6b451088f36f5735eb82815"
|
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.96.0.tgz#e5e72201abdccdda94b952ef605163ca11772d8f"
|
||||||
integrity sha512-OlUHWrGRctRGEm+yL9iWSC9HRnxZhlm3enP2iCKytVmt7LvaPtsK4RtZ27qp4irNvuzg79aqF+h5IFnG+Vi7WA==
|
integrity sha512-WNAxP+0xBtUNgEXrL8aW6UQMmD6WoX9My0VW6uq+Q262DOPTU3zPtWl+9vvES4pF3tPJCFvmFAlK/Alw9+XKVQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.3.1"
|
classnames "^2.3.1"
|
||||||
date-fns "^2.29.3"
|
date-fns "^2.29.3"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue