mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
2e2b1411bf
3
.github/workflows/stale-issues.yml
vendored
3
.github/workflows/stale-issues.yml
vendored
@ -19,5 +19,6 @@ jobs:
|
|||||||
close-issue-message: 'This issue was closed because it has been inactive for 7 days since being marked as stale.'
|
close-issue-message: 'This issue was closed because it has been inactive for 7 days since being marked as stale.'
|
||||||
days-before-pr-stale: -1
|
days-before-pr-stale: -1
|
||||||
days-before-pr-close: -1
|
days-before-pr-close: -1
|
||||||
operations-per-run: 500
|
operations-per-run: 200
|
||||||
|
ascending: true
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
@ -3,7 +3,7 @@ import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
|
|||||||
import { endOfYear, isSameDay } from 'date-fns';
|
import { endOfYear, isSameDay } from 'date-fns';
|
||||||
import DatePickerForm from 'components/metrics/DatePickerForm';
|
import DatePickerForm from 'components/metrics/DatePickerForm';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import { dateFormat } from 'lib/date';
|
import { formatDate } from 'lib/date';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
@ -135,8 +135,8 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
|
|||||||
<Icons.Calendar />
|
<Icons.Calendar />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>
|
<Text>
|
||||||
{dateFormat(startDate, 'd LLL y', locale)}
|
{formatDate(startDate, 'd LLL y', locale)}
|
||||||
{!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`}
|
{!isSameDay(startDate, endDate) && ` — ${formatDate(endDate, 'd LLL y', locale)}`}
|
||||||
</Text>
|
</Text>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
|
51
components/input/MonthSelect.js
Normal file
51
components/input/MonthSelect.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { Text, Icon, CalendarMonthSelect, CalendarYearSelect, Button } from 'react-basics';
|
||||||
|
import { startOfMonth, endOfMonth } from 'date-fns';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import { useLocale } from 'hooks';
|
||||||
|
import { formatDate } from 'lib/date';
|
||||||
|
import { getDateLocale } from 'lib/lang';
|
||||||
|
import styles from './MonthSelect.module.css';
|
||||||
|
|
||||||
|
const MONTH = 'month';
|
||||||
|
const YEAR = 'year';
|
||||||
|
|
||||||
|
export function MonthSelect({ date = new Date(), onChange }) {
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const [select, setSelect] = useState(null);
|
||||||
|
const month = formatDate(date, 'MMMM', locale);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const ref = useRef();
|
||||||
|
|
||||||
|
const handleSelect = value => {
|
||||||
|
setSelect(state => (state !== value ? value : null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = date => {
|
||||||
|
onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`);
|
||||||
|
setSelect(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={ref} className={styles.container}>
|
||||||
|
<Button className={styles.input} variant="quiet" onClick={() => handleSelect(MONTH)}>
|
||||||
|
<Text>{month}</Text>
|
||||||
|
<Icon size="sm">{select === MONTH ? <Icons.Close /> : <Icons.ChevronDown />}</Icon>
|
||||||
|
</Button>
|
||||||
|
<Button className={styles.input} variant="quiet" onClick={() => handleSelect(YEAR)}>
|
||||||
|
<Text>{year}</Text>
|
||||||
|
<Icon size="sm">{select === YEAR ? <Icons.Close /> : <Icons.ChevronDown />}</Icon>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{select === MONTH && (
|
||||||
|
<CalendarMonthSelect date={date} locale={getDateLocale(locale)} onSelect={handleChange} />
|
||||||
|
)}
|
||||||
|
{select === YEAR && (
|
||||||
|
<CalendarYearSelect date={date} locale={getDateLocale(locale)} onSelect={handleChange} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MonthSelect;
|
12
components/input/MonthSelect.module.css
Normal file
12
components/input/MonthSelect.module.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: 40px;
|
padding-top: 40px;
|
||||||
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@ -36,6 +36,7 @@ export function EventDataValueTable({ data = [], event }) {
|
|||||||
<GridColumn name="dataType" label={formatMessage(labels.type)}>
|
<GridColumn name="dataType" label={formatMessage(labels.type)}>
|
||||||
{row => DATA_TYPES[row.dataType]}
|
{row => DATA_TYPES[row.dataType]}
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
|
<GridColumn name="fieldValue" label={formatMessage(labels.value)} />
|
||||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)} width="200px">
|
<GridColumn name="total" label={formatMessage(labels.totalRecords)} width="200px">
|
||||||
{({ total }) => total.toLocaleString()}
|
{({ total }) => total.toLocaleString()}
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
|
@ -8,7 +8,7 @@ import useLocale from 'hooks/useLocale';
|
|||||||
import useCountryNames from 'hooks/useCountryNames';
|
import useCountryNames from 'hooks/useCountryNames';
|
||||||
import { BROWSERS } from 'lib/constants';
|
import { BROWSERS } from 'lib/constants';
|
||||||
import { stringToColor } from 'lib/format';
|
import { stringToColor } from 'lib/format';
|
||||||
import { dateFormat } from 'lib/date';
|
import { formatDate } from 'lib/date';
|
||||||
import { safeDecodeURI } from 'next-basics';
|
import { safeDecodeURI } from 'next-basics';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import styles from './RealtimeLog.module.css';
|
import styles from './RealtimeLog.module.css';
|
||||||
@ -50,7 +50,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getTime = ({ createdAt }) => dateFormat(new Date(createdAt), 'pp', locale);
|
const getTime = ({ createdAt }) => formatDate(new Date(createdAt), 'pp', locale);
|
||||||
|
|
||||||
const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
|
const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
|
||||||
|
|
||||||
|
@ -6,7 +6,12 @@ import { useContext } from 'react';
|
|||||||
import { ReportContext } from './Report';
|
import { ReportContext } from './Report';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
|
|
||||||
export function BaseParameters() {
|
export function BaseParameters({
|
||||||
|
showWebsiteSelect = true,
|
||||||
|
allowWebsiteSelect = true,
|
||||||
|
showDateSelect = true,
|
||||||
|
allowDateSelect = true,
|
||||||
|
}) {
|
||||||
const { report, updateReport } = useContext(ReportContext);
|
const { report, updateReport } = useContext(ReportContext);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
@ -24,17 +29,25 @@ export function BaseParameters() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormRow label={formatMessage(labels.website)}>
|
{showWebsiteSelect && (
|
||||||
<WebsiteSelect websiteId={websiteId} onSelect={handleWebsiteSelect} />
|
<FormRow label={formatMessage(labels.website)}>
|
||||||
</FormRow>
|
{allowWebsiteSelect && (
|
||||||
<FormRow label={formatMessage(labels.dateRange)}>
|
<WebsiteSelect websiteId={websiteId} onSelect={handleWebsiteSelect} />
|
||||||
<DateFilter
|
)}
|
||||||
value={value}
|
</FormRow>
|
||||||
startDate={startDate}
|
)}
|
||||||
endDate={endDate}
|
{showDateSelect && (
|
||||||
onChange={handleDateChange}
|
<FormRow label={formatMessage(labels.dateRange)}>
|
||||||
/>
|
{allowDateSelect && (
|
||||||
</FormRow>
|
<DateFilter
|
||||||
|
value={value}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
import { useContext, useRef } from 'react';
|
import { useContext, useRef } from 'react';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics';
|
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
|
||||||
import { ReportContext } from 'components/pages/reports/Report';
|
import { ReportContext } from 'components/pages/reports/Report';
|
||||||
|
import { MonthSelect } from 'components/input/MonthSelect';
|
||||||
import BaseParameters from '../BaseParameters';
|
import BaseParameters from '../BaseParameters';
|
||||||
|
import { parseDateRange } from 'lib/date';
|
||||||
const fieldOptions = [
|
|
||||||
{ name: 'daily', type: 'string' },
|
|
||||||
{ name: 'weekly', type: 'string' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function RetentionParameters() {
|
export function RetentionParameters() {
|
||||||
const { report, runReport, isRunning } = useContext(ReportContext);
|
const { report, runReport, isRunning, updateReport } = useContext(ReportContext);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
const { parameters } = report || {};
|
const { parameters } = report || {};
|
||||||
const { websiteId, dateRange } = parameters || {};
|
const { websiteId, dateRange } = parameters || {};
|
||||||
|
const { startDate } = dateRange || {};
|
||||||
const queryDisabled = !websiteId || !dateRange;
|
const queryDisabled = !websiteId || !dateRange;
|
||||||
|
|
||||||
const handleSubmit = (data, e) => {
|
const handleSubmit = (data, e) => {
|
||||||
@ -26,9 +24,16 @@ export function RetentionParameters() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDateChange = value => {
|
||||||
|
updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||||
<BaseParameters />
|
<BaseParameters showDateSelect={false} />
|
||||||
|
<FormRow label={formatMessage(labels.dateRange)}>
|
||||||
|
<MonthSelect date={startDate} onChange={handleDateChange} />
|
||||||
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
||||||
{formatMessage(labels.runQuery)}
|
{formatMessage(labels.runQuery)}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { GridTable, GridColumn } from 'react-basics';
|
import { GridTable, GridColumn } from 'react-basics';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../Report';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
import { dateFormat } from 'lib/date';
|
import { formatDate } from 'lib/date';
|
||||||
import styles from './RetentionTable.module.css';
|
import styles from './RetentionTable.module.css';
|
||||||
|
|
||||||
export function RetentionTable() {
|
export function RetentionTable() {
|
||||||
@ -15,34 +16,32 @@ export function RetentionTable() {
|
|||||||
return <EmptyPlaceholder />;
|
return <EmptyPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dates = data.reduce((arr, { date }) => {
|
const rows = data.reduce((arr, { date, visitors }) => {
|
||||||
if (!arr.includes(date)) {
|
if (!arr.find(a => a.date === date)) {
|
||||||
return arr.concat(date);
|
return arr.concat({ date, visitors });
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const days = Array(32).fill(null);
|
const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 30];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.table}>
|
<div className={styles.table}>
|
||||||
<div className={styles.row}>
|
<div className={classNames(styles.row, styles.header)}>
|
||||||
<div className={styles.date}>{formatMessage(labels.date)}</div>
|
<div className={styles.date}>{formatMessage(labels.date)}</div>
|
||||||
{days.map((n, i) => (
|
<div className={styles.visitors}>{formatMessage(labels.visitors)}</div>
|
||||||
<div key={i} className={styles.header}>
|
{days.map(n => (
|
||||||
{formatMessage(labels.day)} {i}
|
<div key={n} className={styles.day}>
|
||||||
|
{formatMessage(labels.day)} {n}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{dates.map((date, i) => {
|
{rows.map(({ date, visitors }, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i} className={styles.row}>
|
<div key={i} className={styles.row}>
|
||||||
<div className={styles.date}>
|
<div className={styles.date}>{formatDate(`${date} 00:00:00`, 'PP')}</div>
|
||||||
{dateFormat(date, 'P')}
|
<div className={styles.visitors}>{visitors}</div>
|
||||||
<br />
|
|
||||||
{date}
|
|
||||||
</div>
|
|
||||||
{days.map((n, day) => {
|
{days.map((n, day) => {
|
||||||
return (
|
return (
|
||||||
<div key={day} className={styles.cell}>
|
<div key={day} className={styles.cell}>
|
||||||
|
@ -4,10 +4,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
width: 60px;
|
font-weight: 700;
|
||||||
height: 40px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
@ -28,5 +25,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
min-width: 200px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitors {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
@ -5,18 +5,18 @@ import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetric
|
|||||||
import { useDateRange, useApi, usePageQuery } from 'hooks';
|
import { useDateRange, useApi, usePageQuery } from 'hooks';
|
||||||
import styles from './WebsiteEventData.module.css';
|
import styles from './WebsiteEventData.module.css';
|
||||||
|
|
||||||
function useData(websiteId, eventName) {
|
function useData(websiteId, event) {
|
||||||
const [dateRange] = useDateRange(websiteId);
|
const [dateRange] = useDateRange(websiteId);
|
||||||
const { startDate, endDate } = dateRange;
|
const { startDate, endDate } = dateRange;
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, error, isLoading } = useQuery(
|
const { data, error, isLoading } = useQuery(
|
||||||
['event-data:events', { websiteId, startDate, endDate, eventName }],
|
['event-data:events', { websiteId, startDate, endDate, event }],
|
||||||
() =>
|
() =>
|
||||||
get('/event-data/events', {
|
get('/event-data/events', {
|
||||||
websiteId,
|
websiteId,
|
||||||
startAt: +startDate,
|
startAt: +startDate,
|
||||||
endAt: +endDate,
|
endAt: +endDate,
|
||||||
eventName,
|
event,
|
||||||
}),
|
}),
|
||||||
{ enabled: !!(websiteId && startDate && endDate) },
|
{ enabled: !!(websiteId && startDate && endDate) },
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { StatusLight } from 'react-basics';
|
import { StatusLight } from 'react-basics';
|
||||||
import { dateFormat } from 'lib/date';
|
import { formatDate } from 'lib/date';
|
||||||
import { formatLongNumber } from 'lib/format';
|
import { formatLongNumber } from 'lib/format';
|
||||||
|
|
||||||
export function renderNumberLabels(label) {
|
export function renderNumberLabels(label) {
|
||||||
@ -12,15 +12,15 @@ export function renderDateLabels(unit, locale) {
|
|||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'minute':
|
case 'minute':
|
||||||
return dateFormat(d, 'h:mm', locale);
|
return formatDate(d, 'h:mm', locale);
|
||||||
case 'hour':
|
case 'hour':
|
||||||
return dateFormat(d, 'p', locale);
|
return formatDate(d, 'p', locale);
|
||||||
case 'day':
|
case 'day':
|
||||||
return dateFormat(d, 'MMM d', locale);
|
return formatDate(d, 'MMM d', locale);
|
||||||
case 'month':
|
case 'month':
|
||||||
return dateFormat(d, 'MMM', locale);
|
return formatDate(d, 'MMM', locale);
|
||||||
case 'year':
|
case 'year':
|
||||||
return dateFormat(d, 'YYY', locale);
|
return formatDate(d, 'YYY', locale);
|
||||||
default:
|
default:
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ export function renderStatusTooltipPopup(unit, locale) {
|
|||||||
|
|
||||||
setTooltipPopup(
|
setTooltipPopup(
|
||||||
<>
|
<>
|
||||||
<div>{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
<div>{formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
||||||
<div>
|
<div>
|
||||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||||
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
||||||
|
@ -63,12 +63,12 @@ function getDateFormat(date) {
|
|||||||
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapFilter(column, operator, name) {
|
function mapFilter(column, operator, name, type = 'String') {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case OPERATORS.equals:
|
case OPERATORS.equals:
|
||||||
return `${column} = {${name}:String}`;
|
return `${column} = {${name}:${type}`;
|
||||||
case OPERATORS.notEquals:
|
case OPERATORS.notEquals:
|
||||||
return `${column} != {${name}:String}`;
|
return `${column} != {${name}:${type}}`;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -249,7 +249,7 @@ export const customFormats = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function dateFormat(date, str, locale = 'en-US') {
|
export function formatDate(date, str, locale = 'en-US') {
|
||||||
return format(
|
return format(
|
||||||
typeof date === 'string' ? new Date(date) : date,
|
typeof date === 'string' ? new Date(date) : date,
|
||||||
customFormats?.[locale]?.[str] || str,
|
customFormats?.[locale]?.[str] || str,
|
||||||
|
@ -92,12 +92,12 @@ function getTimestampIntervalQuery(field: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapFilter(column, operator, name) {
|
function mapFilter(column, operator, name, type = 'String') {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case OPERATORS.equals:
|
case OPERATORS.equals:
|
||||||
return `${column} = {{${name}}}`;
|
return `${column} = {${name}:${type}`;
|
||||||
case OPERATORS.notEquals:
|
case OPERATORS.notEquals:
|
||||||
return `${column} != {{${name}}}`;
|
return `${column} != {${name}:${type}}`;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -126,13 +126,8 @@ export interface WebsiteEventMetric {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebsiteEventDataStats {
|
export interface WebsiteEventData {
|
||||||
fieldName: string;
|
eventName?: string;
|
||||||
dataType: number;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebsiteEventDataFields {
|
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
dataType: number;
|
dataType: number;
|
||||||
fieldValue?: string;
|
fieldValue?: string;
|
||||||
|
@ -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.92.0",
|
"react-basics": "^0.94.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",
|
||||||
|
@ -5,16 +5,17 @@ import { NextApiResponse } from 'next';
|
|||||||
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
import { getEventDataEvents } from 'queries';
|
import { getEventDataEvents } from 'queries';
|
||||||
|
|
||||||
export interface EventDataFieldsRequestBody {
|
export interface EventDataEventsRequestQuery {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
dateRange: {
|
dateRange: {
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
};
|
};
|
||||||
|
event?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
req: NextApiRequestQueryBody<any, EventDataFieldsRequestBody>,
|
req: NextApiRequestQueryBody<EventDataEventsRequestQuery>,
|
||||||
res: NextApiResponse<any>,
|
res: NextApiResponse<any>,
|
||||||
) => {
|
) => {
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
|
@ -5,7 +5,7 @@ import { NextApiResponse } from 'next';
|
|||||||
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
import { getEventDataFields } from 'queries';
|
import { getEventDataFields } from 'queries';
|
||||||
|
|
||||||
export interface EventDataFieldsRequestBody {
|
export interface EventDataFieldsRequestQuery {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
dateRange: {
|
dateRange: {
|
||||||
startDate: string;
|
startDate: string;
|
||||||
@ -15,7 +15,7 @@ export interface EventDataFieldsRequestBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
req: NextApiRequestQueryBody<any, EventDataFieldsRequestBody>,
|
req: NextApiRequestQueryBody<EventDataFieldsRequestQuery>,
|
||||||
res: NextApiResponse<any>,
|
res: NextApiResponse<any>,
|
||||||
) => {
|
) => {
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
|
@ -5,7 +5,7 @@ import { NextApiResponse } from 'next';
|
|||||||
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
import { getEventDataFields } from 'queries';
|
import { getEventDataFields } from 'queries';
|
||||||
|
|
||||||
export interface EventDataRequestBody {
|
export interface EventDataStatsRequestQuery {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
dateRange: {
|
dateRange: {
|
||||||
startDate: string;
|
startDate: string;
|
||||||
@ -15,7 +15,7 @@ export interface EventDataRequestBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
req: NextApiRequestQueryBody<any, EventDataRequestBody>,
|
req: NextApiRequestQueryBody<EventDataStatsRequestQuery>,
|
||||||
res: NextApiResponse<any>,
|
res: NextApiResponse<any>,
|
||||||
) => {
|
) => {
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
@ -32,18 +32,18 @@ export default async (
|
|||||||
const endDate = new Date(+endAt);
|
const endDate = new Date(+endAt);
|
||||||
|
|
||||||
const results = await getEventDataFields(websiteId, { startDate, endDate });
|
const results = await getEventDataFields(websiteId, { startDate, endDate });
|
||||||
const events = new Set();
|
const fields = new Set();
|
||||||
|
|
||||||
const data = results.reduce(
|
const data = results.reduce(
|
||||||
(obj, row) => {
|
(obj, row) => {
|
||||||
events.add(row.fieldName);
|
fields.add(row.fieldName);
|
||||||
obj.records += Number(row.total);
|
obj.records += Number(row.total);
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
{ fields: results.length, records: 0 },
|
{ events: results.length, records: 0 },
|
||||||
);
|
);
|
||||||
|
|
||||||
return ok(res, { ...data, events: events.size });
|
return ok(res, { ...data, fields: fields.size });
|
||||||
}
|
}
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
return methodNotAllowed(res);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import clickhouse from 'lib/clickhouse';
|
import clickhouse from 'lib/clickhouse';
|
||||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||||
import { QueryFilters, WebsiteEventDataFields } from 'lib/types';
|
import { QueryFilters, WebsiteEventData } from 'lib/types';
|
||||||
|
|
||||||
export async function getEventDataEvents(
|
export async function getEventDataEvents(
|
||||||
...args: [websiteId: string, filters: QueryFilters]
|
...args: [websiteId: string, filters: QueryFilters]
|
||||||
): Promise<WebsiteEventDataFields[]> {
|
): Promise<WebsiteEventData[]> {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
@ -24,7 +24,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||||||
website_event.event_name as "eventName",
|
website_event.event_name as "eventName",
|
||||||
event_data.event_key as "fieldName",
|
event_data.event_key as "fieldName",
|
||||||
event_data.data_type as "dataType",
|
event_data.data_type as "dataType",
|
||||||
event_data.string_value as "value",
|
event_data.string_value as "fieldValue",
|
||||||
count(*) as "total"
|
count(*) as "total"
|
||||||
from event_data
|
from event_data
|
||||||
inner join website_event
|
inner join website_event
|
||||||
@ -71,7 +71,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
|||||||
event_name as eventName,
|
event_name as eventName,
|
||||||
event_key as fieldName,
|
event_key as fieldName,
|
||||||
data_type as dataType,
|
data_type as dataType,
|
||||||
string_value as value,
|
string_value as fieldValue,
|
||||||
count(*) as total
|
count(*) as total
|
||||||
from event_data
|
from event_data
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import clickhouse from 'lib/clickhouse';
|
import clickhouse from 'lib/clickhouse';
|
||||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||||
import { QueryFilters, WebsiteEventDataFields } from 'lib/types';
|
import { QueryFilters, WebsiteEventData } from 'lib/types';
|
||||||
|
|
||||||
export async function getEventDataFields(
|
export async function getEventDataFields(
|
||||||
...args: [websiteId: string, filters: QueryFilters & { field?: string }]
|
...args: [websiteId: string, filters: QueryFilters & { field?: string }]
|
||||||
): Promise<WebsiteEventDataFields[]> {
|
): Promise<WebsiteEventData[]> {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
|
@ -5,9 +5,10 @@ import prisma from 'lib/prisma';
|
|||||||
export async function getRetention(
|
export async function getRetention(
|
||||||
...args: [
|
...args: [
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
dateRange: {
|
filters: {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
timezone: string;
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
@ -19,9 +20,10 @@ export async function getRetention(
|
|||||||
|
|
||||||
async function relationalQuery(
|
async function relationalQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
dateRange: {
|
filters: {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
timezone: string;
|
||||||
},
|
},
|
||||||
): Promise<
|
): Promise<
|
||||||
{
|
{
|
||||||
@ -32,9 +34,8 @@ async function relationalQuery(
|
|||||||
percentage: number;
|
percentage: number;
|
||||||
}[]
|
}[]
|
||||||
> {
|
> {
|
||||||
const { startDate, endDate } = dateRange;
|
const { startDate, endDate, timezone = 'UTC' } = filters;
|
||||||
const { getDateQuery, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
|
const { getDateQuery, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
|
||||||
const timezone = 'utc';
|
|
||||||
const unit = 'day';
|
const unit = 'day';
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
@ -85,7 +86,7 @@ async function relationalQuery(
|
|||||||
from cohort_date c
|
from cohort_date c
|
||||||
join cohort_size s
|
join cohort_size s
|
||||||
on c.cohort_date = s.cohort_date
|
on c.cohort_date = s.cohort_date
|
||||||
where c.day_number IN (0,1,2,3,4,5,6,7,14,21,30)
|
where c.day_number <= 31
|
||||||
order by 1, 2`,
|
order by 1, 2`,
|
||||||
{
|
{
|
||||||
websiteId,
|
websiteId,
|
||||||
@ -99,9 +100,10 @@ async function relationalQuery(
|
|||||||
|
|
||||||
async function clickhouseQuery(
|
async function clickhouseQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
dateRange: {
|
filters: {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
timezone: string;
|
||||||
},
|
},
|
||||||
): Promise<
|
): Promise<
|
||||||
{
|
{
|
||||||
@ -112,9 +114,8 @@ async function clickhouseQuery(
|
|||||||
percentage: number;
|
percentage: number;
|
||||||
}[]
|
}[]
|
||||||
> {
|
> {
|
||||||
const { startDate, endDate } = dateRange;
|
const { startDate, endDate, timezone = 'UTC' } = filters;
|
||||||
const { getDateQuery, getDateStringQuery, rawQuery } = clickhouse;
|
const { getDateQuery, getDateStringQuery, rawQuery } = clickhouse;
|
||||||
const timezone = 'UTC';
|
|
||||||
const unit = 'day';
|
const unit = 'day';
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
@ -164,7 +165,7 @@ async function clickhouseQuery(
|
|||||||
from cohort_date c
|
from cohort_date c
|
||||||
join cohort_size s
|
join cohort_size s
|
||||||
on c.cohort_date = s.cohort_date
|
on c.cohort_date = s.cohort_date
|
||||||
where c.day_number IN (0,1,2,3,4,5,6,7,14,21,30)
|
where c.day_number <= 31
|
||||||
order by 1, 2`,
|
order by 1, 2`,
|
||||||
{
|
{
|
||||||
websiteId,
|
websiteId,
|
||||||
|
@ -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.92.0:
|
react-basics@^0.94.0:
|
||||||
version "0.92.0"
|
version "0.94.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b"
|
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.94.0.tgz#c15698148b959f40c6b451088f36f5735eb82815"
|
||||||
integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA==
|
integrity sha512-OlUHWrGRctRGEm+yL9iWSC9HRnxZhlm3enP2iCKytVmt7LvaPtsK4RtZ27qp4irNvuzg79aqF+h5IFnG+Vi7WA==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.3.1"
|
classnames "^2.3.1"
|
||||||
date-fns "^2.29.3"
|
date-fns "^2.29.3"
|
||||||
|
Loading…
Reference in New Issue
Block a user