From c548267d91409db46d42894953dd57b4cfbecd48 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 16 Aug 2023 10:50:28 -0700 Subject: [PATCH] Added month select component. --- components/input/DateFilter.js | 6 +-- components/input/MonthSelect.js | 51 +++++++++++++++++++ components/input/MonthSelect.module.css | 12 +++++ components/layout/SettingsLayout.module.css | 1 + components/pages/realtime/RealtimeLog.js | 4 +- components/pages/reports/BaseParameters.js | 37 +++++++++----- .../reports/retention/RetentionParameters.js | 21 +++++--- .../pages/reports/retention/RetentionTable.js | 29 +++++------ .../retention/RetentionTable.module.css | 26 ++++++++-- lib/charts.js | 14 ++--- lib/clickhouse.ts | 6 +-- lib/date.js | 2 +- lib/prisma.ts | 6 +-- package.json | 2 +- queries/analytics/reports/getRetention.ts | 15 +++--- yarn.lock | 8 +-- 16 files changed, 169 insertions(+), 71 deletions(-) create mode 100644 components/input/MonthSelect.js create mode 100644 components/input/MonthSelect.module.css 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/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/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 efce3f4e..6a6c1790 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -67,12 +67,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/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/queries/analytics/reports/getRetention.ts b/queries/analytics/reports/getRetention.ts index ee7e4619..7473e042 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, rawQuery } = prisma; - const timezone = 'utc'; const unit = 'day'; return rawQuery( @@ -94,9 +95,10 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - dateRange: { + filters: { startDate: Date; endDate: Date; + timezone: string; }, ): Promise< { @@ -107,9 +109,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( 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"