Skip to content

Commit

Permalink
feat: date range selector (#8991)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Dec 18, 2024
1 parent 4503510 commit da16b31
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 23 deletions.
81 changes: 81 additions & 0 deletions frontend/src/component/common/FilterDateItem/DateRangePresets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
Box,
List,
ListItem,
ListItemButton,
styled,
Typography,
} from '@mui/material';
import type { FilterItemParams } from '../../filter/FilterItem/FilterItem';
import type { FC } from 'react';
import { calculateDateRange, type RangeType } from './calculateDateRange';

export const PresetsHeader = styled(Typography)(({ theme }) => ({
paddingLeft: theme.spacing(2),
paddingBottom: theme.spacing(1),
}));

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 (
<Box>
<PresetsHeader variant='h3'>Presets</PresetsHeader>
<List disablePadding sx={{ pb: 2 }}>
<ListItem disablePadding>
<ListItemButton onClick={rangeChangeHandler('thisMonth')}>
This month
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton
onClick={rangeChangeHandler('previousMonth')}
>
Previous month
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton onClick={rangeChangeHandler('thisQuarter')}>
This quarter
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton
onClick={rangeChangeHandler('previousQuarter')}
>
Previous quarter
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton onClick={rangeChangeHandler('thisYear')}>
This year
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton
onClick={rangeChangeHandler('previousYear')}
>
Previous year
</ListItemButton>
</ListItem>
</List>
</Box>
);
};
23 changes: 17 additions & 6 deletions frontend/src/component/common/FilterDateItem/FilterDateItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ 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;
onChipClose: () => void;
onRangeChange?: (value: {
from: FilterItemParams;
to: FilterItemParams;
}) => void;
onChipClose?: () => void;
state: FilterItemParams | null | undefined;
operators: [string, ...string[]];
}
Expand All @@ -22,6 +27,7 @@ export const FilterDateItem: FC<IFilterDateItemProps> = ({
name,
label,
onChange,
onRangeChange,
onChipClose,
state,
operators,
Expand Down Expand Up @@ -54,11 +60,13 @@ export const FilterDateItem: FC<IFilterDateItemProps> = ({
: [];
const selectedDate = state ? new Date(state.values[0]) : null;
const currentOperator = state ? state.operator : operators[0];
const onDelete = () => {
onChange({ operator: operators[0], values: [] });
onClose();
onChipClose();
};
const onDelete = onChipClose
? () => {
onChange({ operator: operators[0], values: [] });
onClose();
onChipClose();
}
: undefined;

useEffect(() => {
if (state && !operators.includes(state.operator)) {
Expand Down Expand Up @@ -115,6 +123,9 @@ export const FilterDateItem: FC<IFilterDateItemProps> = ({
});
}}
/>
{onRangeChange && (
<DateRangePresets onRangeChange={onRangeChange} />
)}
</LocalizationProvider>
</StyledPopover>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
66 changes: 66 additions & 0 deletions frontend/src/component/common/FilterDateItem/calculateDateRange.ts
Original file line number Diff line number Diff line change
@@ -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')];
};
6 changes: 6 additions & 0 deletions frontend/src/component/events/EventLog/EventLogFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,19 @@ export const useEventLogFilters = (
options: [],
filterKey: 'from',
dateOperators: ['IS'],
fromFilterKey: 'from',
toFilterKey: 'to',
persistent: true,
},
{
label: 'Date To',
icon: 'today',
options: [],
filterKey: 'to',
dateOperators: ['IS'],
fromFilterKey: 'from',
toFilterKey: 'to',
persistent: true,
},
{
label: 'Created by',
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/component/events/EventLog/useEventLogSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import mapValues from 'lodash.mapvalues';
import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch';
import type { SearchEventsParams } from 'openapi';
import type { FilterItemParamHolder } from 'component/filter/Filters/Filters';
import { format, subMonths } from 'date-fns';

type Log =
| { type: 'global' }
Expand Down Expand Up @@ -60,8 +61,14 @@ export const useEventLogSearch = (
offset: withDefault(NumberParam, 0),
limit: withDefault(NumberParam, DEFAULT_PAGE_SIZE),
query: StringParam,
from: FilterItemParam,
to: FilterItemParam,
from: withDefault(FilterItemParam, {
values: [format(subMonths(new Date(), 1), 'yyyy-MM-dd')],
operator: 'IS',
}),
to: withDefault(FilterItemParam, {
values: [format(new Date(), 'yyyy-MM-dd')],
operator: 'IS',
}),
createdBy: FilterItemParam,
type: FilterItemParam,
...extraParameters(logType),
Expand All @@ -81,6 +88,7 @@ export const useEventLogSearch = (
const [tableState, setTableState] = usePersistentTableState(
fullStorageKey,
stateConfig,
['from', 'to', 'offset'],
);

const filterState = (() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const StyledLabel = styled('div')(({ theme }) => ({
alignItems: 'center',
justifyContent: 'space-between',
fontWeight: theme.typography.fontWeightBold,
minHeight: theme.spacing(3.5),
}));

const StyledOptions = styled('button')(({ theme }) => ({
Expand Down
32 changes: 28 additions & 4 deletions frontend/src/component/filter/Filters/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type ITextFilterItem = IBaseFilterItem & {

type IDateFilterItem = IBaseFilterItem & {
dateOperators: [string, ...string[]];
fromFilterKey?: string;
toFilterKey?: string;
persistent?: boolean;
};

export type IFilterItem = ITextFilterItem | IDateFilterItem;
Expand Down Expand Up @@ -116,6 +119,22 @@ export const Filters: FC<IFilterProps> = ({
}, [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 (
<StyledBox className={className}>
{selectedFilters.map((selectedFilter) => {
Expand Down Expand Up @@ -143,11 +162,16 @@ export const Filters: FC<IFilterProps> = ({
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)}
onChipClose={
filter.persistent
? undefined
: () => deselectFilter(filter.label)
}
/>
);
}
Expand Down
19 changes: 11 additions & 8 deletions frontend/src/component/insights/Insights.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ test('Filter insights by project and date', async () => {
render(<Insights withCharts={false} />);
const addFilter = await screen.findByText('Add Filter');
fireEvent.click(addFilter);

const dateFromFilter = await screen.findByText('Date From');
await screen.findByText('Date To');
const projectFilter = await screen.findByText('Project');

// filter by project
Expand All @@ -45,11 +42,17 @@ test('Filter insights by project and date', async () => {
await fireEvent.click(projectName);
expect(window.location.href).toContain('project=IS%3AprojectB');

// filter by from date
fireEvent.click(dateFromFilter);
const day = await screen.findByText('25');
fireEvent.click(day);
// last month moving window by default
const fromDate = await screen.findByText('03/25/2024');
await screen.findByText('04/25/2024');

// change dates by preset range
fireEvent.click(fromDate);
const previousMonth = await screen.findByText('Previous month');
fireEvent.click(previousMonth);
await screen.findByText('03/01/2024');
await screen.findByText('03/31/2024');
expect(window.location.href).toContain(
'project=IS%3AprojectB&from=IS%3A2024-04-25',
'?project=IS%3AprojectB&from=IS%3A2024-03-01&to=IS%3A2024-03-31',
);
});
Loading

0 comments on commit da16b31

Please sign in to comment.