diff --git a/frontend/src/component/common/FilterDateItem/DateRangePresets.tsx b/frontend/src/component/common/FilterDateItem/DateRangePresets.tsx
new file mode 100644
index 000000000000..2eb92c2f133f
--- /dev/null
+++ b/frontend/src/component/common/FilterDateItem/DateRangePresets.tsx
@@ -0,0 +1,71 @@
+import { Box, List, ListItem, ListItemButton, Typography } from '@mui/material';
+import type { FilterItemParams } from '../../filter/FilterItem/FilterItem';
+import type { FC } from 'react';
+import { calculateDateRange, type RangeType } from './calculateDateRange';
+
+export const DateRangePresets: FC<{
+ onRangeChange: (value: {
+ from: FilterItemParams;
+ to: FilterItemParams;
+ }) => void;
+}> = ({ onRangeChange }) => {
+ const rangeChangeHandler = (rangeType: RangeType) => () => {
+ const [start, end] = calculateDateRange(rangeType);
+ onRangeChange({
+ from: {
+ operator: 'IS',
+ values: [start],
+ },
+ to: {
+ operator: 'IS',
+ values: [end],
+ },
+ });
+ };
+
+ return (
+
+
+ Presets
+
+
+
+
+ This month
+
+
+
+
+ Previous month
+
+
+
+
+ This quarter
+
+
+
+
+ Previous quarter
+
+
+
+
+ This year
+
+
+
+
+ Previous year
+
+
+
+
+ );
+};
diff --git a/frontend/src/component/common/FilterDateItem/FilterDateItem.tsx b/frontend/src/component/common/FilterDateItem/FilterDateItem.tsx
index 7e063519afd5..4f193555511a 100644
--- a/frontend/src/component/common/FilterDateItem/FilterDateItem.tsx
+++ b/frontend/src/component/common/FilterDateItem/FilterDateItem.tsx
@@ -8,11 +8,16 @@ import { format } from 'date-fns';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { getLocalizedDateString } from '../util';
import type { FilterItemParams } from 'component/filter/FilterItem/FilterItem';
+import { DateRangePresets } from './DateRangePresets';
export interface IFilterDateItemProps {
name: string;
label: ReactNode;
onChange: (value: FilterItemParams) => void;
+ onRangeChange?: (value: {
+ from: FilterItemParams;
+ to: FilterItemParams;
+ }) => void;
onChipClose: () => void;
state: FilterItemParams | null | undefined;
operators: [string, ...string[]];
@@ -22,6 +27,7 @@ export const FilterDateItem: FC = ({
name,
label,
onChange,
+ onRangeChange,
onChipClose,
state,
operators,
@@ -115,6 +121,9 @@ export const FilterDateItem: FC = ({
});
}}
/>
+ {onRangeChange && (
+
+ )}
>
diff --git a/frontend/src/component/common/FilterDateItem/calculateDateRange.test.ts b/frontend/src/component/common/FilterDateItem/calculateDateRange.test.ts
new file mode 100644
index 000000000000..7051cafd2480
--- /dev/null
+++ b/frontend/src/component/common/FilterDateItem/calculateDateRange.test.ts
@@ -0,0 +1,40 @@
+import { calculateDateRange, type RangeType } from './calculateDateRange';
+
+describe('calculateDateRange', () => {
+ const fixedDate = new Date('2024-06-16');
+
+ test.each<[RangeType, string, string]>([
+ ['thisMonth', '2024-06-01', '2024-06-30'],
+ ['previousMonth', '2024-05-01', '2024-05-31'],
+ ['thisQuarter', '2024-04-01', '2024-06-30'],
+ ['previousQuarter', '2024-01-01', '2024-03-31'],
+ ['thisYear', '2024-01-01', '2024-12-31'],
+ ['previousYear', '2023-01-01', '2023-12-31'],
+ ])(
+ 'should return correct range for %s',
+ (rangeType, expectedStart, expectedEnd) => {
+ const [start, end] = calculateDateRange(rangeType, fixedDate);
+ expect(start).toBe(expectedStart);
+ expect(end).toBe(expectedEnd);
+ },
+ );
+
+ test('should default to previousMonth if rangeType is invalid', () => {
+ const [start, end] = calculateDateRange(
+ 'invalidRange' as RangeType,
+ fixedDate,
+ );
+ expect(start).toBe('2024-05-01');
+ expect(end).toBe('2024-05-31');
+ });
+
+ test('should handle edge case for previousMonth at year boundary', () => {
+ const yearBoundaryDate = new Date('2024-01-15');
+ const [start, end] = calculateDateRange(
+ 'previousMonth',
+ yearBoundaryDate,
+ );
+ expect(start).toBe('2023-12-01');
+ expect(end).toBe('2023-12-31');
+ });
+});
diff --git a/frontend/src/component/common/FilterDateItem/calculateDateRange.ts b/frontend/src/component/common/FilterDateItem/calculateDateRange.ts
new file mode 100644
index 000000000000..29c77d4e9105
--- /dev/null
+++ b/frontend/src/component/common/FilterDateItem/calculateDateRange.ts
@@ -0,0 +1,66 @@
+import {
+ endOfMonth,
+ endOfQuarter,
+ endOfYear,
+ format,
+ startOfMonth,
+ startOfQuarter,
+ startOfYear,
+ subMonths,
+ subQuarters,
+ subYears,
+} from 'date-fns';
+
+export type RangeType =
+ | 'thisMonth'
+ | 'previousMonth'
+ | 'thisQuarter'
+ | 'previousQuarter'
+ | 'thisYear'
+ | 'previousYear';
+
+export const calculateDateRange = (
+ rangeType: RangeType,
+ today = new Date(),
+): [string, string] => {
+ let start: Date;
+ let end: Date;
+
+ switch (rangeType) {
+ case 'thisMonth': {
+ start = startOfMonth(today);
+ end = endOfMonth(today);
+ break;
+ }
+ case 'thisQuarter': {
+ start = startOfQuarter(today);
+ end = endOfQuarter(today);
+ break;
+ }
+ case 'previousQuarter': {
+ const previousQuarter = subQuarters(today, 1);
+ start = startOfQuarter(previousQuarter);
+ end = endOfQuarter(previousQuarter);
+ break;
+ }
+ case 'thisYear': {
+ start = startOfYear(today);
+ end = endOfYear(today);
+ break;
+ }
+ case 'previousYear': {
+ const lastYear = subYears(today, 1);
+ start = startOfYear(lastYear);
+ end = endOfYear(lastYear);
+ break;
+ }
+
+ default: {
+ const lastMonth = subMonths(today, 1);
+ start = startOfMonth(lastMonth);
+ end = endOfMonth(lastMonth);
+ }
+ }
+
+ return [format(start, 'yyyy-MM-dd'), format(end, 'yyyy-MM-dd')];
+};
diff --git a/frontend/src/component/events/EventLog/EventLogFilters.tsx b/frontend/src/component/events/EventLog/EventLogFilters.tsx
index 702598abd93b..4cd75e7229f8 100644
--- a/frontend/src/component/events/EventLog/EventLogFilters.tsx
+++ b/frontend/src/component/events/EventLog/EventLogFilters.tsx
@@ -57,6 +57,8 @@ export const useEventLogFilters = (
options: [],
filterKey: 'from',
dateOperators: ['IS'],
+ fromFilterKey: 'from',
+ toFilterKey: 'to',
},
{
label: 'Date To',
@@ -64,6 +66,8 @@ export const useEventLogFilters = (
options: [],
filterKey: 'to',
dateOperators: ['IS'],
+ fromFilterKey: 'from',
+ toFilterKey: 'to',
},
{
label: 'Created by',
diff --git a/frontend/src/component/filter/Filters/Filters.tsx b/frontend/src/component/filter/Filters/Filters.tsx
index 472e22145f2d..a5c67918ea72 100644
--- a/frontend/src/component/filter/Filters/Filters.tsx
+++ b/frontend/src/component/filter/Filters/Filters.tsx
@@ -41,6 +41,8 @@ type ITextFilterItem = IBaseFilterItem & {
type IDateFilterItem = IBaseFilterItem & {
dateOperators: [string, ...string[]];
+ fromFilterKey?: string;
+ toFilterKey?: string;
};
export type IFilterItem = ITextFilterItem | IDateFilterItem;
@@ -116,6 +118,22 @@ export const Filters: FC = ({
}, [JSON.stringify(state), JSON.stringify(availableFilters)]);
const hasAvailableFilters = unselectedFilters.length > 0;
+
+ const rangeChangeHandler = (filter: IDateFilterItem) => {
+ const fromKey = filter.fromFilterKey;
+ const toKey = filter.toFilterKey;
+ if (fromKey && toKey) {
+ return (value: {
+ from: FilterItemParams;
+ to: FilterItemParams;
+ }) => {
+ onChange({ [fromKey]: value.from });
+ onChange({ [toKey]: value.to });
+ };
+ }
+ return undefined;
+ };
+
return (
{selectedFilters.map((selectedFilter) => {
@@ -143,9 +161,10 @@ export const Filters: FC = ({
label={label}
name={filter.label}
state={state[filter.filterKey]}
- onChange={(value) =>
- onChange({ [filter.filterKey]: value })
- }
+ onChange={(value) => {
+ onChange({ [filter.filterKey]: value });
+ }}
+ onRangeChange={rangeChangeHandler(filter)}
operators={filter.dateOperators}
onChipClose={() => deselectFilter(filter.label)}
/>
diff --git a/frontend/src/component/insights/InsightsFilters.tsx b/frontend/src/component/insights/InsightsFilters.tsx
index 99ecb9a3114c..c3f231d36706 100644
--- a/frontend/src/component/insights/InsightsFilters.tsx
+++ b/frontend/src/component/insights/InsightsFilters.tsx
@@ -34,6 +34,8 @@ export const InsightsFilters: FC = ({
options: [],
filterKey: 'from',
dateOperators: ['IS'],
+ fromFilterKey: 'from',
+ toFilterKey: 'to',
},
{
label: 'Date To',
@@ -41,6 +43,8 @@ export const InsightsFilters: FC = ({
options: [],
filterKey: 'to',
dateOperators: ['IS'],
+ fromFilterKey: 'from',
+ toFilterKey: 'to',
},
...(hasMultipleProjects
? ([