diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 52c0d432..24711fba 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -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.' days-before-pr-stale: -1 days-before-pr-close: -1 - operations-per-run: 500 + operations-per-run: 200 + ascending: true repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/components/input/DateFilter.js b/components/input/DateFilter.js index 7fc4319d..af4b69dd 100644 --- a/components/input/DateFilter.js +++ b/components/input/DateFilter.js @@ -3,7 +3,7 @@ import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { endOfYear, isSameDay } from 'date-fns'; import DatePickerForm from 'components/metrics/DatePickerForm'; import useLocale from 'hooks/useLocale'; -import { dateFormat } from 'lib/date'; +import { formatDate } from 'lib/date'; import Icons from 'components/icons'; import useMessages from 'hooks/useMessages'; @@ -135,8 +135,8 @@ const CustomRange = ({ startDate, endDate, onClick }) => { - {dateFormat(startDate, 'd LLL y', locale)} - {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} + {formatDate(startDate, 'd LLL y', locale)} + {!isSameDay(startDate, endDate) && ` — ${formatDate(endDate, 'd LLL y', locale)}`} ); diff --git a/components/input/MonthSelect.js b/components/input/MonthSelect.js new file mode 100644 index 00000000..bb054446 --- /dev/null +++ b/components/input/MonthSelect.js @@ -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 ( + <> +
+ + +
+ {select === MONTH && ( + + )} + {select === YEAR && ( + + )} + + ); +} + +export default MonthSelect; diff --git a/components/input/MonthSelect.module.css b/components/input/MonthSelect.module.css new file mode 100644 index 00000000..04cf575c --- /dev/null +++ b/components/input/MonthSelect.module.css @@ -0,0 +1,12 @@ +.container { + display: flex; + align-items: center; + justify-content: center; +} + +.input { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; +} diff --git a/components/layout/SettingsLayout.module.css b/components/layout/SettingsLayout.module.css index 569b903b..36d029f0 100644 --- a/components/layout/SettingsLayout.module.css +++ b/components/layout/SettingsLayout.module.css @@ -2,6 +2,7 @@ display: flex; flex-direction: column; padding-top: 40px; + padding-right: 20px; } .content { diff --git a/components/pages/event-data/EventDataValueTable.js b/components/pages/event-data/EventDataValueTable.js index 3688ad09..69ed10a7 100644 --- a/components/pages/event-data/EventDataValueTable.js +++ b/components/pages/event-data/EventDataValueTable.js @@ -36,6 +36,7 @@ export function EventDataValueTable({ data = [], event }) { {row => DATA_TYPES[row.dataType]} + {({ total }) => total.toLocaleString()} diff --git a/components/pages/realtime/RealtimeLog.js b/components/pages/realtime/RealtimeLog.js index 744bff00..6486f707 100644 --- a/components/pages/realtime/RealtimeLog.js +++ b/components/pages/realtime/RealtimeLog.js @@ -8,7 +8,7 @@ import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; import { stringToColor } from 'lib/format'; -import { dateFormat } from 'lib/date'; +import { formatDate } from 'lib/date'; import { safeDecodeURI } from 'next-basics'; import Icons from 'components/icons'; 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); diff --git a/components/pages/reports/BaseParameters.js b/components/pages/reports/BaseParameters.js index 394432cf..76c35a58 100644 --- a/components/pages/reports/BaseParameters.js +++ b/components/pages/reports/BaseParameters.js @@ -6,7 +6,12 @@ import { useContext } from 'react'; import { ReportContext } from './Report'; import { useMessages } from 'hooks'; -export function BaseParameters() { +export function BaseParameters({ + showWebsiteSelect = true, + allowWebsiteSelect = true, + showDateSelect = true, + allowDateSelect = true, +}) { const { report, updateReport } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); @@ -24,17 +29,25 @@ export function BaseParameters() { return ( <> - - - - - - + {showWebsiteSelect && ( + + {allowWebsiteSelect && ( + + )} + + )} + {showDateSelect && ( + + {allowDateSelect && ( + + )} + + )} ); } diff --git a/components/pages/reports/retention/RetentionParameters.js b/components/pages/reports/retention/RetentionParameters.js index f6bde0b1..1eee6bf2 100644 --- a/components/pages/reports/retention/RetentionParameters.js +++ b/components/pages/reports/retention/RetentionParameters.js @@ -1,21 +1,19 @@ import { useContext, useRef } from 'react'; 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 { MonthSelect } from 'components/input/MonthSelect'; import BaseParameters from '../BaseParameters'; - -const fieldOptions = [ - { name: 'daily', type: 'string' }, - { name: 'weekly', type: 'string' }, -]; +import { parseDateRange } from 'lib/date'; export function RetentionParameters() { - const { report, runReport, isRunning } = useContext(ReportContext); + const { report, runReport, isRunning, updateReport } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); const ref = useRef(null); const { parameters } = report || {}; const { websiteId, dateRange } = parameters || {}; + const { startDate } = dateRange || {}; const queryDisabled = !websiteId || !dateRange; const handleSubmit = (data, e) => { @@ -26,9 +24,16 @@ export function RetentionParameters() { } }; + const handleDateChange = value => { + updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } }); + }; + return (
- + + + + {formatMessage(labels.runQuery)} diff --git a/components/pages/reports/retention/RetentionTable.js b/components/pages/reports/retention/RetentionTable.js index 01a84a01..f7d8c4bb 100644 --- a/components/pages/reports/retention/RetentionTable.js +++ b/components/pages/reports/retention/RetentionTable.js @@ -1,9 +1,10 @@ import { useContext } from 'react'; import { GridTable, GridColumn } from 'react-basics'; +import classNames from 'classnames'; import { ReportContext } from '../Report'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import { useMessages } from 'hooks'; -import { dateFormat } from 'lib/date'; +import { formatDate } from 'lib/date'; import styles from './RetentionTable.module.css'; export function RetentionTable() { @@ -15,34 +16,32 @@ export function RetentionTable() { return ; } - const dates = data.reduce((arr, { date }) => { - if (!arr.includes(date)) { - return arr.concat(date); + const rows = data.reduce((arr, { date, visitors }) => { + if (!arr.find(a => a.date === date)) { + return arr.concat({ date, visitors }); } return arr; }, []); - const days = Array(32).fill(null); + const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 30]; return ( <>
-
+
{formatMessage(labels.date)}
- {days.map((n, i) => ( -
- {formatMessage(labels.day)} {i} +
{formatMessage(labels.visitors)}
+ {days.map(n => ( +
+ {formatMessage(labels.day)} {n}
))}
- {dates.map((date, i) => { + {rows.map(({ date, visitors }, i) => { return (
-
- {dateFormat(date, 'P')} -
- {date} -
+
{formatDate(`${date} 00:00:00`, 'PP')}
+
{visitors}
{days.map((n, day) => { return (
diff --git a/components/pages/reports/retention/RetentionTable.module.css b/components/pages/reports/retention/RetentionTable.module.css index 0943ffc0..79cbbc5f 100644 --- a/components/pages/reports/retention/RetentionTable.module.css +++ b/components/pages/reports/retention/RetentionTable.module.css @@ -4,10 +4,7 @@ } .header { - width: 60px; - height: 40px; - text-align: center; - font-size: var(--font-size-sm); + font-weight: 700; } .row { @@ -28,5 +25,24 @@ } .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; } diff --git a/components/pages/websites/WebsiteEventData.js b/components/pages/websites/WebsiteEventData.js index 5e208355..7f9a6829 100644 --- a/components/pages/websites/WebsiteEventData.js +++ b/components/pages/websites/WebsiteEventData.js @@ -5,18 +5,18 @@ import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetric import { useDateRange, useApi, usePageQuery } from 'hooks'; import styles from './WebsiteEventData.module.css'; -function useData(websiteId, eventName) { +function useData(websiteId, event) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate } = dateRange; const { get, useQuery } = useApi(); const { data, error, isLoading } = useQuery( - ['event-data:events', { websiteId, startDate, endDate, eventName }], + ['event-data:events', { websiteId, startDate, endDate, event }], () => get('/event-data/events', { websiteId, startAt: +startDate, endAt: +endDate, - eventName, + event, }), { enabled: !!(websiteId && startDate && endDate) }, ); diff --git a/lib/charts.js b/lib/charts.js index 0571a9a9..ff746cb5 100644 --- a/lib/charts.js +++ b/lib/charts.js @@ -1,5 +1,5 @@ import { StatusLight } from 'react-basics'; -import { dateFormat } from 'lib/date'; +import { formatDate } from 'lib/date'; import { formatLongNumber } from 'lib/format'; export function renderNumberLabels(label) { @@ -12,15 +12,15 @@ export function renderDateLabels(unit, locale) { switch (unit) { case 'minute': - return dateFormat(d, 'h:mm', locale); + return formatDate(d, 'h:mm', locale); case 'hour': - return dateFormat(d, 'p', locale); + return formatDate(d, 'p', locale); case 'day': - return dateFormat(d, 'MMM d', locale); + return formatDate(d, 'MMM d', locale); case 'month': - return dateFormat(d, 'MMM', locale); + return formatDate(d, 'MMM', locale); case 'year': - return dateFormat(d, 'YYY', locale); + return formatDate(d, 'YYY', locale); default: return label; } @@ -50,7 +50,7 @@ export function renderStatusTooltipPopup(unit, locale) { setTooltipPopup( <> -
{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}
+
{formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label} diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 75786850..aa2f21ed 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -63,12 +63,12 @@ function getDateFormat(date) { return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`; } -function mapFilter(column, operator, name) { +function mapFilter(column, operator, name, type = 'String') { switch (operator) { case OPERATORS.equals: - return `${column} = {${name}:String}`; + return `${column} = {${name}:${type}`; case OPERATORS.notEquals: - return `${column} != {${name}:String}`; + return `${column} != {${name}:${type}}`; default: return ''; } diff --git a/lib/date.js b/lib/date.js index 8a023822..49bff897 100644 --- a/lib/date.js +++ b/lib/date.js @@ -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( typeof date === 'string' ? new Date(date) : date, customFormats?.[locale]?.[str] || str, diff --git a/lib/prisma.ts b/lib/prisma.ts index 5a21a6ec..cebd7193 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -92,12 +92,12 @@ function getTimestampIntervalQuery(field: string): string { } } -function mapFilter(column, operator, name) { +function mapFilter(column, operator, name, type = 'String') { switch (operator) { case OPERATORS.equals: - return `${column} = {{${name}}}`; + return `${column} = {${name}:${type}`; case OPERATORS.notEquals: - return `${column} != {{${name}}}`; + return `${column} != {${name}:${type}}`; default: return ''; } diff --git a/lib/types.ts b/lib/types.ts index 65bef8fb..3f3ac533 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -126,13 +126,8 @@ export interface WebsiteEventMetric { y: number; } -export interface WebsiteEventDataStats { - fieldName: string; - dataType: number; - total: number; -} - -export interface WebsiteEventDataFields { +export interface WebsiteEventData { + eventName?: string; fieldName: string; dataType: number; fieldValue?: string; diff --git a/package.json b/package.json index 89dc5e97..46ad4d2d 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.92.0", + "react-basics": "^0.94.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/pages/api/event-data/events.ts b/pages/api/event-data/events.ts index e83e541b..9f8f964b 100644 --- a/pages/api/event-data/events.ts +++ b/pages/api/event-data/events.ts @@ -5,16 +5,17 @@ import { NextApiResponse } from 'next'; import { ok, methodNotAllowed, unauthorized } from 'next-basics'; import { getEventDataEvents } from 'queries'; -export interface EventDataFieldsRequestBody { +export interface EventDataEventsRequestQuery { websiteId: string; dateRange: { startDate: string; endDate: string; }; + event?: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/event-data/fields.ts b/pages/api/event-data/fields.ts index f21bd570..b6a73133 100644 --- a/pages/api/event-data/fields.ts +++ b/pages/api/event-data/fields.ts @@ -5,7 +5,7 @@ import { NextApiResponse } from 'next'; import { ok, methodNotAllowed, unauthorized } from 'next-basics'; import { getEventDataFields } from 'queries'; -export interface EventDataFieldsRequestBody { +export interface EventDataFieldsRequestQuery { websiteId: string; dateRange: { startDate: string; @@ -15,7 +15,7 @@ export interface EventDataFieldsRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/event-data/stats.ts b/pages/api/event-data/stats.ts index 74f420c4..d1ee396b 100644 --- a/pages/api/event-data/stats.ts +++ b/pages/api/event-data/stats.ts @@ -5,7 +5,7 @@ import { NextApiResponse } from 'next'; import { ok, methodNotAllowed, unauthorized } from 'next-basics'; import { getEventDataFields } from 'queries'; -export interface EventDataRequestBody { +export interface EventDataStatsRequestQuery { websiteId: string; dateRange: { startDate: string; @@ -15,7 +15,7 @@ export interface EventDataRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); @@ -32,18 +32,18 @@ export default async ( const endDate = new Date(+endAt); const results = await getEventDataFields(websiteId, { startDate, endDate }); - const events = new Set(); + const fields = new Set(); const data = results.reduce( (obj, row) => { - events.add(row.fieldName); + fields.add(row.fieldName); obj.records += Number(row.total); 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); diff --git a/queries/analytics/eventData/getEventDataEvents.ts b/queries/analytics/eventData/getEventDataEvents.ts index 25084111..2c8cb0e0 100644 --- a/queries/analytics/eventData/getEventDataEvents.ts +++ b/queries/analytics/eventData/getEventDataEvents.ts @@ -1,11 +1,11 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventDataFields } from 'lib/types'; +import { QueryFilters, WebsiteEventData } from 'lib/types'; export async function getEventDataEvents( ...args: [websiteId: string, filters: QueryFilters] -): Promise { +): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -24,7 +24,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { website_event.event_name as "eventName", event_data.event_key as "fieldName", event_data.data_type as "dataType", - event_data.string_value as "value", + event_data.string_value as "fieldValue", count(*) as "total" from event_data inner join website_event @@ -71,7 +71,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { event_name as eventName, event_key as fieldName, data_type as dataType, - string_value as value, + string_value as fieldValue, count(*) as total from event_data where website_id = {websiteId:UUID} diff --git a/queries/analytics/eventData/getEventDataFields.ts b/queries/analytics/eventData/getEventDataFields.ts index f5f426e0..ac32b188 100644 --- a/queries/analytics/eventData/getEventDataFields.ts +++ b/queries/analytics/eventData/getEventDataFields.ts @@ -1,11 +1,11 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventDataFields } from 'lib/types'; +import { QueryFilters, WebsiteEventData } from 'lib/types'; export async function getEventDataFields( ...args: [websiteId: string, filters: QueryFilters & { field?: string }] -): Promise { +): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), diff --git a/queries/analytics/reports/getRetention.ts b/queries/analytics/reports/getRetention.ts index 3abc0e7a..3c384b6e 100644 --- a/queries/analytics/reports/getRetention.ts +++ b/queries/analytics/reports/getRetention.ts @@ -5,9 +5,10 @@ import prisma from 'lib/prisma'; export async function getRetention( ...args: [ websiteId: string, - dateRange: { + filters: { startDate: Date; endDate: Date; + timezone: string; }, ] ) { @@ -19,9 +20,10 @@ export async function getRetention( async function relationalQuery( websiteId: string, - dateRange: { + filters: { startDate: Date; endDate: Date; + timezone: string; }, ): Promise< { @@ -32,9 +34,8 @@ async function relationalQuery( percentage: number; }[] > { - const { startDate, endDate } = dateRange; + const { startDate, endDate, timezone = 'UTC' } = filters; const { getDateQuery, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma; - const timezone = 'utc'; const unit = 'day'; return rawQuery( @@ -85,7 +86,7 @@ async function relationalQuery( from cohort_date c join cohort_size s 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`, { websiteId, @@ -99,9 +100,10 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - dateRange: { + filters: { startDate: Date; endDate: Date; + timezone: string; }, ): Promise< { @@ -112,9 +114,8 @@ async function clickhouseQuery( percentage: number; }[] > { - const { startDate, endDate } = dateRange; + const { startDate, endDate, timezone = 'UTC' } = filters; const { getDateQuery, getDateStringQuery, rawQuery } = clickhouse; - const timezone = 'UTC'; const unit = 'day'; return rawQuery( @@ -164,7 +165,7 @@ async function clickhouseQuery( from cohort_date c join cohort_size s 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`, { websiteId, diff --git a/yarn.lock b/yarn.lock index 115e3cc9..e67cc413 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,10 +7557,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.92.0: - version "0.92.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b" - integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA== +react-basics@^0.94.0: + version "0.94.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.94.0.tgz#c15698148b959f40c6b451088f36f5735eb82815" + integrity sha512-OlUHWrGRctRGEm+yL9iWSC9HRnxZhlm3enP2iCKytVmt7LvaPtsK4RtZ27qp4irNvuzg79aqF+h5IFnG+Vi7WA== dependencies: classnames "^2.3.1" date-fns "^2.29.3"