Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(flags): Add relative date operators #19792

Merged
merged 26 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions frontend/src/lib/components/DateFilter/DateFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,25 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { dateFilterToText, dateMapping, uuid } from 'lib/utils'
import { useRef } from 'react'

import { DateMappingOption } from '~/types'
import { DateMappingOption, PropertyOperator } from '~/types'

import { PropertyFilterDatePicker } from '../PropertyFilters/components/PropertyFilterDatePicker'
import { dateFilterLogic } from './dateFilterLogic'
import { RollingDateRangeFilter } from './RollingDateRangeFilter'

export interface DateFilterProps {
showCustom?: boolean
showRollingRangePicker?: boolean
makeLabel?: (key: React.ReactNode) => React.ReactNode
makeLabel?: (key: React.ReactNode, startOfRange?: React.ReactNode) => React.ReactNode
className?: string
onChange?: (fromDate: string | null, toDate: string | null) => void
disabled?: boolean
dateOptions?: DateMappingOption[]
isDateFormatted?: boolean
size?: LemonButtonProps['size']
dropdownPlacement?: Placement
/* True when we're not dealing with ranges, but a single date / relative date */
isFixedDateMode?: boolean
}
interface RawDateFilterProps extends DateFilterProps {
dateFrom?: string | null | dayjs.Dayjs
Expand All @@ -53,6 +56,7 @@ export function DateFilter({
size,
dropdownPlacement = 'bottom-start',
max,
isFixedDateMode = false,
}: RawDateFilterProps): JSX.Element {
const key = useRef(uuid()).current
const logicProps: DateFilterLogicProps = {
Expand All @@ -62,11 +66,30 @@ export function DateFilter({
onChange,
dateOptions,
isDateFormatted,
isFixedDateMode,
}
const { open, openFixedRange, openDateToNow, close, setRangeDateFrom, setRangeDateTo, setDate, applyRange } =
useActions(dateFilterLogic(logicProps))
const { isVisible, view, rangeDateFrom, rangeDateTo, label, isFixedRange, isDateToNow, isRollingDateRange } =
useValues(dateFilterLogic(logicProps))
const {
open,
openFixedRange,
openDateToNow,
openFixedDate,
close,
setRangeDateFrom,
setRangeDateTo,
setDate,
applyRange,
} = useActions(dateFilterLogic(logicProps))
const {
isVisible,
view,
rangeDateFrom,
rangeDateTo,
label,
isFixedRange,
isDateToNow,
isFixedDate,
isRollingDateRange,
} = useValues(dateFilterLogic(logicProps))

const optionsRef = useRef<HTMLDivElement | null>(null)
const rollingDateRangeRef = useRef<HTMLDivElement | null>(null)
Expand All @@ -93,6 +116,15 @@ export function DateFilter({
}}
onClose={open}
/>
) : view === DateFilterView.FixedDate ? (
<PropertyFilterDatePicker
autoFocus
operator={PropertyOperator.Exact}
value={rangeDateFrom ? rangeDateFrom.toString() : dayjs().toString()}
setValue={(date) => {
setDate(String(date), '')
}}
/>
) : (
<div className="space-y-px" ref={optionsRef} onClick={(e) => e.stopPropagation()}>
{dateOptions.map(({ key, values, inactive }) => {
Expand All @@ -115,7 +147,17 @@ export function DateFilter({
)

return (
<Tooltip key={key} title={makeLabel ? makeLabel(dateValue) : undefined}>
<Tooltip
key={key}
title={
makeLabel
? makeLabel(
dateValue,
dateFilterToText(values[0], undefined, '', [], false, 'MMMM D, YYYY', true)
neilkakkar marked this conversation as resolved.
Show resolved Hide resolved
)
: undefined
}
>
<LemonButton
key={key}
onClick={() => setDate(values[0] || null, values[1] || null)}
Expand All @@ -129,7 +171,9 @@ export function DateFilter({
})}
{showRollingRangePicker && (
<RollingDateRangeFilter
pageKey={key}
dateFrom={dateFrom}
dateRangeFilterLabel={isFixedDateMode ? 'The last' : undefined}
selected={isRollingDateRange}
onChange={(fromDate) => {
setDate(fromDate, '')
Expand All @@ -139,15 +183,24 @@ export function DateFilter({
ref: rollingDateRangeRef,
}}
max={max}
allowedDateOptions={isFixedDateMode ? ['hours', 'days', 'weeks', 'months', 'years'] : undefined}
/>
)}
<LemonDivider />
<LemonButton onClick={openDateToNow} active={isDateToNow} fullWidth>
From custom date until now…
</LemonButton>
<LemonButton onClick={openFixedRange} active={isFixedRange} fullWidth>
Custom fixed date range…
</LemonButton>
{isFixedDateMode ? (
<LemonButton onClick={openFixedDate} active={isFixedDate} fullWidth>
Custom date...
</LemonButton>
) : (
<>
<LemonButton onClick={openDateToNow} active={isDateToNow} fullWidth>
From custom date until now…
</LemonButton>
<LemonButton onClick={openFixedRange} active={isFixedRange} fullWidth>
Custom fixed date range…
</LemonButton>
</>
)}
</div>
)

Expand Down
22 changes: 16 additions & 6 deletions frontend/src/lib/components/DateFilter/RollingDateRangeFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,26 @@
import { DateOption, rollingDateRangeFilterLogic } from './rollingDateRangeFilterLogic'

const dateOptions: LemonSelectOptions<DateOption> = [
{ value: 'hours', label: 'hours' },
{ value: 'days', label: 'days' },
{ value: 'weeks', label: 'weeks' },
{ value: 'months', label: 'months' },
{ value: 'quarters', label: 'quarters' },
{ value: 'years', label: 'years' },
]

type RollingDateRangeFilterProps = {
pageKey?: string
selected?: boolean
dateFrom?: string | null | dayjs.Dayjs
max?: number | null
onChange?: (fromDate: string) => void
makeLabel?: (key: React.ReactNode) => React.ReactNode
makeLabel?: (key: React.ReactNode, startOfRange?: React.ReactNode) => React.ReactNode
popover?: {
ref?: React.MutableRefObject<HTMLDivElement | null>
}
dateRangeFilterLabel?: string
allowedDateOptions?: DateOption[]
}

export function RollingDateRangeFilter({
Expand All @@ -32,21 +37,24 @@
dateFrom,
selected,
max,
dateRangeFilterLabel = 'In the last',
pageKey,
allowedDateOptions = ['days', 'weeks', 'months', 'years'],
}: RollingDateRangeFilterProps): JSX.Element {
const logicProps = { onChange, dateFrom, selected, max }
const logicProps = { onChange, dateFrom, selected, max, pageKey }
const { increaseCounter, decreaseCounter, setCounter, setDateOption, toggleDateOptionsSelector, select } =
useActions(rollingDateRangeFilterLogic(logicProps))
const { counter, dateOption, formattedDate } = useValues(rollingDateRangeFilterLogic(logicProps))
const { counter, dateOption, formattedDate, startOfDateRange } = useValues(rollingDateRangeFilterLogic(logicProps))

return (
<Tooltip title={makeLabel ? makeLabel(formattedDate) : undefined}>
<Tooltip title={makeLabel ? makeLabel(formattedDate, startOfDateRange) : undefined}>
<LemonButton
className="RollingDateRangeFilter"
data-attr="rolling-date-range-filter"
onClick={select}
active={selected}
>
<p className="RollingDateRangeFilter__label">In the last</p>
<p className="RollingDateRangeFilter__label">{dateRangeFilterLabel}</p>
<div className="RollingDateRangeFilter__counter" onClick={(e): void => e.stopPropagation()}>
<span
className="RollingDateRangeFilter__counter__step"
Expand Down Expand Up @@ -82,7 +90,9 @@
toggleDateOptionsSelector()
}}
dropdownMatchSelectWidth={false}
options={dateOptions}
options={dateOptions.filter((option) =>

Check failure on line 93 in frontend/src/lib/components/DateFilter/RollingDateRangeFilter.tsx

View workflow job for this annotation

GitHub Actions / Code quality checks

This expression is not callable.

Check failure on line 93 in frontend/src/lib/components/DateFilter/RollingDateRangeFilter.tsx

View workflow job for this annotation

GitHub Actions / Code quality checks

Parameter 'option' implicitly has an 'any' type.
'value' in option ? allowedDateOptions.includes(option.value) : true
)}
menu={{
...popover,
className: 'RollingDateRangeFilter__popover',
Expand Down
23 changes: 17 additions & 6 deletions frontend/src/lib/components/DateFilter/dateFilterLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const dateFilterLogic = kea<dateFilterLogicType>([
open: true,
openFixedRange: true,
openDateToNow: true,
openFixedDate: true,
close: true,
applyRange: true,
setDate: (dateFrom: string | null, dateTo: string | null) => ({ dateFrom, dateTo }),
Expand All @@ -28,6 +29,7 @@ export const dateFilterLogic = kea<dateFilterLogicType>([
open: () => DateFilterView.QuickList,
openFixedRange: () => DateFilterView.FixedRange,
openDateToNow: () => DateFilterView.DateToNow,
openFixedDate: () => DateFilterView.FixedDate,
},
],
isVisible: [
Expand All @@ -36,6 +38,7 @@ export const dateFilterLogic = kea<dateFilterLogicType>([
open: () => true,
openFixedRange: () => true,
openDateToNow: () => true,
openFixedDate: () => true,
setDate: () => false,
close: () => false,
},
Expand Down Expand Up @@ -71,27 +74,35 @@ export const dateFilterLogic = kea<dateFilterLogicType>([
(dateFrom, dateTo) => !!(dateFrom && dateTo && dayjs(dateFrom).isValid() && dayjs(dateTo).isValid()),
],
isDateToNow: [
(s) => [s.dateFrom, s.dateTo],
(dateFrom, dateTo) => !!dateFrom && !dateTo && dayjs(dateFrom).isValid(),
(s) => [s.dateFrom, s.dateTo, (_, p) => p.isFixedDateMode],
(dateFrom, dateTo, isFixedDateMode) =>
!!dateFrom && !dateTo && dayjs(dateFrom).isValid() && !isFixedDateMode,
],
isFixedDate: [
(s) => [s.dateFrom, s.dateTo, (_, p) => p.isFixedDateMode],
(dateFrom, dateTo, isFixedDateMode) => dateFrom && dayjs(dateFrom).isValid() && !dateTo && isFixedDateMode,
],
isRollingDateRange: [
(s) => [s.isFixedRange, s.isDateToNow, s.dateOptions, s.dateFrom, s.dateTo],
(isFixedRange, isDateToNow, dateOptions, dateFrom, dateTo): boolean =>
(s) => [s.isFixedRange, s.isDateToNow, s.isFixedDate, s.dateOptions, s.dateFrom, s.dateTo],
(isFixedRange, isDateToNow, isFixedDate, dateOptions, dateFrom, dateTo): boolean =>
!isFixedRange &&
!isDateToNow &&
!isFixedDate &&
!dateOptions?.find(
(option) =>
(option.values[0] ?? null) === (dateFrom ?? null) &&
(option.values[1] ?? null) === (dateTo ?? null)
),
],
label: [
(s) => [s.dateFrom, s.dateTo, s.isFixedRange, s.isDateToNow, s.dateOptions],
(dateFrom, dateTo, isFixedRange, isDateToNow, dateOptions) =>
(s) => [s.dateFrom, s.dateTo, s.isFixedRange, s.isDateToNow, s.isFixedDate, s.dateOptions],
(dateFrom, dateTo, isFixedRange, isDateToNow, isFixedDate, dateOptions) =>
isFixedRange
? formatDateRange(dayjs(dateFrom), dayjs(dateTo))
: isDateToNow
? `${formatDate(dayjs(dateFrom))} to now`
: isFixedDate
? formatDate(dateStringToDayJs(dateFrom) ?? dayjs(dateFrom))
: dateFilterToText(dateFrom, dateTo, CUSTOM_OPTION_VALUE, dateOptions, false),
],
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import './RollingDateRangeFilter.scss'

import { actions, kea, listeners, path, props, reducers, selectors } from 'kea'
import { actions, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { Dayjs } from 'lib/dayjs'
import { dateFilterToText } from 'lib/utils'

import type { rollingDateRangeFilterLogicType } from './rollingDateRangeFilterLogicType'

const dateOptionsMap = {
y: 'years',
q: 'quarters',
m: 'months',
w: 'weeks',
d: 'days',
h: 'hours',
} as const

export type DateOption = (typeof dateOptionsMap)[keyof typeof dateOptionsMap]
Expand All @@ -20,6 +22,7 @@ export type RollingDateFilterLogicPropsType = {
onChange?: (fromDate: string) => void
dateFrom?: Dayjs | string | null
max?: number | null
pageKey?: string
}

const counterDefault = (selected: boolean | undefined, dateFrom: Dayjs | string | null | undefined): number => {
Expand All @@ -32,7 +35,7 @@ const counterDefault = (selected: boolean | undefined, dateFrom: Dayjs | string
return 3
}

const dateOptionDefault = (selected: boolean | undefined, dateFrom: Dayjs | string | null | undefined): string => {
const dateOptionDefault = (selected: boolean | undefined, dateFrom: Dayjs | string | null | undefined): DateOption => {
if (selected && dateFrom && typeof dateFrom === 'string') {
const dateOption = dateOptionsMap[dateFrom.slice(-1)]
if (dateOption) {
Expand All @@ -44,15 +47,16 @@ const dateOptionDefault = (selected: boolean | undefined, dateFrom: Dayjs | stri

export const rollingDateRangeFilterLogic = kea<rollingDateRangeFilterLogicType>([
path(['lib', 'components', 'DateFilter', 'RollingDateRangeFilterLogic']),
props({} as RollingDateFilterLogicPropsType),
key(({ pageKey }) => pageKey ?? 'unknown'),
actions({
increaseCounter: true,
decreaseCounter: true,
setCounter: (counter: number | null | undefined) => ({ counter }),
setDateOption: (option: string) => ({ option }),
setDateOption: (option: DateOption) => ({ option }),
toggleDateOptionsSelector: true,
select: true,
}),
props({} as RollingDateFilterLogicPropsType),
reducers(({ props }) => ({
counter: [
counterDefault(props.selected, props.dateFrom) as number | null,
Expand Down Expand Up @@ -84,17 +88,23 @@ export const rollingDateRangeFilterLogic = kea<rollingDateRangeFilterLogicType>(
selectors(() => ({
value: [
(s) => [s.counter, s.dateOption],
(counter: number | null, dateOption: string) => {
(counter, dateOption) => {
if (!counter) {
return ''
}
switch (dateOption) {
case 'years':
return `-${counter}y`
case 'quarters':
return `-${counter}q`
case 'months':
return `-${counter}m`
case 'weeks':
return `-${counter}w`
case 'days':
return `-${counter}d`
case 'hours':
return `-${counter}h`
default:
return `-${counter}d`
}
Expand All @@ -106,6 +116,20 @@ export const rollingDateRangeFilterLogic = kea<rollingDateRangeFilterLogicType>(
return dateFilterToText(value, undefined, 'Custom rolling range', [], true)
},
],
startOfDateRange: [
(s) => [s.value],
(value: string) => {
return dateFilterToText(
value,
undefined,
'N/A',
[],
false,
value.slice(-1) === 'h' ? 'MMMM D, YYYY HH:mm:ss' : 'MMMM D, YYYY',
true
)
},
],
})),
listeners(({ props, values }) => ({
select: () => {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/components/DateFilter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum DateFilterView {
QuickList = 'QuickList',
DateToNow = 'DateToNow',
FixedRange = 'FixedRange',
FixedDate = 'FixedDate',
}

export type DateFilterLogicProps = {
Expand All @@ -15,6 +16,7 @@ export type DateFilterLogicProps = {
dateTo?: Dayjs | string | null
dateOptions?: DateMappingOption[]
isDateFormatted?: boolean
isFixedDateMode?: boolean
}

export const CUSTOM_OPTION_KEY = 'Custom'
Expand Down
Loading
Loading